Merge branch 'layout-align'
This commit is contained in:
+86
-74
@@ -43,7 +43,7 @@ struct IndexEntry {
|
|||||||
reading_minutes: i64,
|
reading_minutes: i64,
|
||||||
summary: String,
|
summary: String,
|
||||||
why: Option<String>,
|
why: Option<String>,
|
||||||
understanding: Option<String>,
|
understanding: Understanding,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct IndexSection {
|
struct IndexSection {
|
||||||
@@ -81,7 +81,7 @@ struct ArticleChapter {
|
|||||||
byline: Option<String>,
|
byline: Option<String>,
|
||||||
meta_line: String,
|
meta_line: String,
|
||||||
social_line: Option<String>,
|
social_line: Option<String>,
|
||||||
understanding: Option<String>,
|
understanding: Understanding,
|
||||||
why: Option<String>,
|
why: Option<String>,
|
||||||
summary: Option<String>,
|
summary: Option<String>,
|
||||||
excerpt_only: bool,
|
excerpt_only: bool,
|
||||||
@@ -209,67 +209,61 @@ fn facet_label(token: &str) -> Option<String> {
|
|||||||
"boston_new_england" => "Boston & New England",
|
"boston_new_england" => "Boston & New England",
|
||||||
"outdoors_lifestyle" => "Outdoors & lifestyle",
|
"outdoors_lifestyle" => "Outdoors & lifestyle",
|
||||||
"history" => "History",
|
"history" => "History",
|
||||||
"reported_news" => "reported news",
|
"reported_news" => "Reported",
|
||||||
"analysis_essay" => "analysis essay",
|
"analysis_essay" => "Analysis",
|
||||||
"how_to_technical" => "how-to",
|
"how_to_technical" => "How-to",
|
||||||
"first_hand_account" => "first-hand account",
|
"first_hand_account" => "First person",
|
||||||
"announcement_roundup" => "announcement",
|
"announcement_roundup" => "Announcement",
|
||||||
"code_repository" => "code repository",
|
"code_repository" => "Code",
|
||||||
"documentation_reference" => "documentation",
|
"documentation_reference" => "Documentation",
|
||||||
"tool_or_product_page" => "product page",
|
"tool_or_product_page" => "Product page",
|
||||||
"discussion_thread" => "discussion thread",
|
"discussion_thread" => "Discussion",
|
||||||
"paper_or_report" => "paper or report",
|
"paper_or_report" => "Paper",
|
||||||
"interview_or_transcript" => "interview",
|
"interview_or_transcript" => "Interview",
|
||||||
"video_or_podcast" => "video or podcast",
|
"video_or_podcast" => "Video or podcast",
|
||||||
"fiction_or_humor" => "fiction or humor",
|
"fiction_or_humor" => "Fiction & humor",
|
||||||
"brief" => "brief",
|
|
||||||
"standard" => "standard depth",
|
|
||||||
"deep" => "in depth",
|
|
||||||
"nontechnical" => "non-technical",
|
|
||||||
"light" => "lightly technical",
|
|
||||||
"intermediate" => "moderately technical",
|
|
||||||
"advanced" => "highly technical",
|
|
||||||
"other" => return None,
|
"other" => return None,
|
||||||
unknown => return Some(unknown.replace('_', " ")),
|
unknown => return Some(unknown.replace('_', " ")),
|
||||||
};
|
};
|
||||||
Some(label.to_string())
|
Some(label.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One muted line saying what the pipeline understood about an article: the
|
#[derive(Clone, Debug, Default, Eq, PartialEq)]
|
||||||
/// deep-assessment facets, the extracted topics and the best-matching reader
|
pub struct Understanding {
|
||||||
/// interests. `None` when there is nothing to say (no deep read, no interests).
|
pub kicker: Option<String>,
|
||||||
pub fn understanding_line(pick: &Pick) -> Option<String> {
|
pub topics: Option<String>,
|
||||||
let mut parts = Vec::new();
|
pub interests: Option<String>,
|
||||||
if let Some(deep) = &pick.llm {
|
}
|
||||||
|
|
||||||
|
/// Reader-facing assessment details, split so templates can give each part the
|
||||||
|
/// same editorial hierarchy across web and EPUB surfaces.
|
||||||
|
pub fn understanding(pick: &Pick) -> Understanding {
|
||||||
|
let (kicker, topics) = pick.llm.as_ref().map_or((None, None), |deep| {
|
||||||
let facets = &deep.facets;
|
let facets = &deep.facets;
|
||||||
for token in [
|
let kicker = [facets.topic_group.as_deref(), facets.format.as_deref()]
|
||||||
facets.topic_group.as_deref(),
|
|
||||||
facets.format.as_deref(),
|
|
||||||
facets.depth.as_deref(),
|
|
||||||
facets.technicality.as_deref(),
|
|
||||||
]
|
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.flatten()
|
.flatten()
|
||||||
{
|
.filter_map(facet_label)
|
||||||
if let Some(label) = facet_label(token).filter(|label| !label.is_empty()) {
|
.filter(|label| !label.is_empty())
|
||||||
parts.push(label);
|
.collect::<Vec<_>>();
|
||||||
}
|
let kicker = (!kicker.is_empty()).then(|| kicker.join(" \u{00b7} "));
|
||||||
}
|
let topics = facets.specific_topics.as_ref().and_then(|topics| {
|
||||||
if let Some(topics) = &facets.specific_topics {
|
|
||||||
let topics = topics
|
let topics = topics
|
||||||
.iter()
|
.iter()
|
||||||
.map(|topic| topic.trim())
|
.map(|topic| topic.trim())
|
||||||
.filter(|topic| !topic.is_empty())
|
.filter(|topic| !topic.is_empty())
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
if !topics.is_empty() {
|
(!topics.is_empty()).then(|| topics.join(" \u{00b7} "))
|
||||||
parts.push(format!("Topics: {}", topics.join(", ")));
|
});
|
||||||
|
(kicker, topics)
|
||||||
|
});
|
||||||
|
let interests = (!pick.top_interests.is_empty()).then(|| pick.top_interests.join(" \u{00b7} "));
|
||||||
|
|
||||||
|
Understanding {
|
||||||
|
kicker,
|
||||||
|
topics,
|
||||||
|
interests,
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
if !pick.top_interests.is_empty() {
|
|
||||||
parts.push(format!("Interests: {}", pick.top_interests.join(", ")));
|
|
||||||
}
|
|
||||||
(!parts.is_empty()).then(|| parts.join(" \u{00b7} "))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn article_href(pick: &Pick) -> String {
|
fn article_href(pick: &Pick) -> String {
|
||||||
@@ -394,7 +388,7 @@ pub fn render_in_this_issue(issue: &Issue) -> Result<Chapter, EpubError> {
|
|||||||
reading_minutes: pick.article.reading_minutes(),
|
reading_minutes: pick.article.reading_minutes(),
|
||||||
summary: summary_for(issue, pick).unwrap_or_default().to_string(),
|
summary: summary_for(issue, pick).unwrap_or_default().to_string(),
|
||||||
why: pick.why.clone(),
|
why: pick.why.clone(),
|
||||||
understanding: understanding_line(pick),
|
understanding: understanding(pick),
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
sections.push(IndexSection { name, entries });
|
sections.push(IndexSection { name, entries });
|
||||||
@@ -409,7 +403,7 @@ pub fn render_in_this_issue(issue: &Issue) -> Result<Chapter, EpubError> {
|
|||||||
reading_minutes: 3,
|
reading_minutes: 3,
|
||||||
summary: "The day's events, as recorded by the Current Events portal.".into(),
|
summary: "The day's events, as recorded by the Current Events portal.".into(),
|
||||||
why: None,
|
why: None,
|
||||||
understanding: None,
|
understanding: Understanding::default(),
|
||||||
}],
|
}],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -485,7 +479,7 @@ pub fn render_article(
|
|||||||
byline: article.author.as_ref().map(|a| format!("By {a}")),
|
byline: article.author.as_ref().map(|a| format!("By {a}")),
|
||||||
meta_line: meta_parts.join(" \u{00b7} "),
|
meta_line: meta_parts.join(" \u{00b7} "),
|
||||||
social_line: social_line(&article.social),
|
social_line: social_line(&article.social),
|
||||||
understanding: understanding_line(pick),
|
understanding: understanding(pick),
|
||||||
why: pick.why.clone(),
|
why: pick.why.clone(),
|
||||||
summary: summary_for(issue, pick).map(str::to_string),
|
summary: summary_for(issue, pick).map(str::to_string),
|
||||||
excerpt_only: article.excerpt_only,
|
excerpt_only: article.excerpt_only,
|
||||||
@@ -810,35 +804,43 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn understanding_line_includes_facets_topics_and_interests() {
|
fn understanding_includes_kicker_topics_and_interests() {
|
||||||
let issue = issue();
|
let issue = issue();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
understanding_line(&issue.lineup.picks[0]).as_deref(),
|
understanding(&issue.lineup.picks[0]),
|
||||||
Some(
|
Understanding {
|
||||||
"Software engineering · analysis essay · in depth · highly technical · Topics: copy-on-write, ZFS · Interests: Filesystems, Rust"
|
kicker: Some("Software engineering · Analysis".into()),
|
||||||
)
|
topics: Some("copy-on-write · ZFS".into()),
|
||||||
|
interests: Some("Filesystems · Rust".into()),
|
||||||
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn understanding_line_is_none_without_a_deep_read_or_interests() {
|
fn understanding_is_empty_without_a_deep_read_or_interests() {
|
||||||
let issue = issue();
|
let issue = issue();
|
||||||
assert!(understanding_line(&issue.lineup.picks[1]).is_none());
|
assert_eq!(
|
||||||
|
understanding(&issue.lineup.picks[1]),
|
||||||
|
Understanding::default()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn understanding_line_can_contain_only_interests() {
|
fn understanding_can_contain_only_interests() {
|
||||||
let issue = issue();
|
let issue = issue();
|
||||||
let mut pick = issue.lineup.picks[1].clone();
|
let mut pick = issue.lineup.picks[1].clone();
|
||||||
pick.top_interests = vec!["Rust".into()];
|
pick.top_interests = vec!["Rust".into()];
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
understanding_line(&pick).as_deref(),
|
understanding(&pick),
|
||||||
Some("Interests: Rust")
|
Understanding {
|
||||||
|
interests: Some("Rust".into()),
|
||||||
|
..Understanding::default()
|
||||||
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn understanding_line_skips_other_topic_group() {
|
fn understanding_skips_other_topic_group() {
|
||||||
let issue = issue();
|
let issue = issue();
|
||||||
let mut pick = issue.lineup.picks[0].clone();
|
let mut pick = issue.lineup.picks[0].clone();
|
||||||
pick.llm.as_mut().unwrap().facets = crate::types::Facets {
|
pick.llm.as_mut().unwrap().facets = crate::types::Facets {
|
||||||
@@ -846,7 +848,7 @@ mod tests {
|
|||||||
..crate::types::Facets::default()
|
..crate::types::Facets::default()
|
||||||
};
|
};
|
||||||
pick.top_interests.clear();
|
pick.top_interests.clear();
|
||||||
assert!(understanding_line(&pick).is_none());
|
assert_eq!(understanding(&pick), Understanding::default());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -883,12 +885,10 @@ mod tests {
|
|||||||
assert!(chapter.xhtml.contains("6 min read"));
|
assert!(chapter.xhtml.contains("6 min read"));
|
||||||
assert!(chapter.xhtml.contains("What it argues"));
|
assert!(chapter.xhtml.contains("What it argues"));
|
||||||
assert!(chapter.xhtml.contains("A short abstract"));
|
assert!(chapter.xhtml.contains("A short abstract"));
|
||||||
// The lead's understanding line is on the index; the second pick has none.
|
// The lead's rubric and matches are on the index; the second pick has neither.
|
||||||
assert_eq!(
|
assert_eq!(chapter.xhtml.matches("class=\"rubric\"").count(), 1);
|
||||||
chapter.xhtml.matches("class=\"index-understood\"").count(),
|
assert!(chapter.xhtml.contains("Software engineering · Analysis"));
|
||||||
1
|
assert!(chapter.xhtml.contains("Matches: Filesystems · Rust"));
|
||||||
);
|
|
||||||
assert!(chapter.xhtml.contains("Interests: Filesystems, Rust"));
|
|
||||||
// Titles are escaped (askama emits numeric references), never injected raw.
|
// Titles are escaped (askama emits numeric references), never injected raw.
|
||||||
assert!(chapter.xhtml.contains("A Niche Delight & Other Tales"));
|
assert!(chapter.xhtml.contains("A Niche Delight & Other Tales"));
|
||||||
assert_xml_ok(&chapter.xhtml);
|
assert_xml_ok(&chapter.xhtml);
|
||||||
@@ -958,10 +958,22 @@ mod tests {
|
|||||||
assert!(chapter.xhtml.contains("/r/2026-08-15/1/down?t="));
|
assert!(chapter.xhtml.contains("/r/2026-08-15/1/down?t="));
|
||||||
assert!(chapter.xhtml.contains("Read online"));
|
assert!(chapter.xhtml.contains("Read online"));
|
||||||
assert!(chapter.xhtml.contains("href=\"disc-1001.xhtml\""));
|
assert!(chapter.xhtml.contains("href=\"disc-1001.xhtml\""));
|
||||||
assert!(chapter.xhtml.contains("class=\"understood\""));
|
assert!(chapter.xhtml.contains("class=\"rubric\""));
|
||||||
assert!(chapter.xhtml.contains(
|
assert!(chapter.xhtml.contains("Software engineering · Analysis"));
|
||||||
"Software engineering · analysis essay · in depth · highly technical · Topics: copy-on-write, ZFS · Interests: Filesystems, Rust"
|
assert!(chapter.xhtml.contains("copy-on-write · ZFS"));
|
||||||
));
|
assert!(chapter.xhtml.contains("Matches: Filesystems · Rust"));
|
||||||
|
let rubric_position = chapter.xhtml.find("class=\"rubric\"").unwrap();
|
||||||
|
let summary_position = chapter.xhtml.find("class=\"summary\"").unwrap();
|
||||||
|
let social_position = chapter.xhtml.find("class=\"social\"").unwrap();
|
||||||
|
assert!(rubric_position < summary_position);
|
||||||
|
assert!(summary_position < social_position);
|
||||||
|
let footer = chapter
|
||||||
|
.xhtml
|
||||||
|
.split_once("<div class=\"article-footer\">")
|
||||||
|
.unwrap()
|
||||||
|
.1;
|
||||||
|
assert!(!footer.contains("class=\"rubric\""));
|
||||||
|
assert!(!footer.contains("Matches:"));
|
||||||
let second = render_article(
|
let second = render_article(
|
||||||
&issue,
|
&issue,
|
||||||
&issue.lineup.picks[1],
|
&issue.lineup.picks[1],
|
||||||
@@ -971,7 +983,7 @@ mod tests {
|
|||||||
Some("s3cret"),
|
Some("s3cret"),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(!second.xhtml.contains("class=\"understood\""));
|
assert!(!second.xhtml.contains("class=\"rubric\""));
|
||||||
// The un-downloaded image degrades to a placeholder.
|
// The un-downloaded image degrades to a placeholder.
|
||||||
assert!(
|
assert!(
|
||||||
chapter
|
chapter
|
||||||
|
|||||||
@@ -7,15 +7,25 @@
|
|||||||
<p class="byline">{{ line }}</p>
|
<p class="byline">{{ line }}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<p class="meta">{{ meta_line }}</p>
|
<p class="meta">{{ meta_line }}</p>
|
||||||
{% if let Some(text) = why %}
|
{% if understanding.kicker.is_some() || understanding.topics.is_some() %}
|
||||||
<p class="why"><em>Why it's here: {{ text }}</em></p>
|
<p class="rubric">{% if let Some(kicker) = understanding.kicker %}<span class="kicker">{{ kicker }}</span>{% if let Some(topics) = understanding.topics %}   {{ topics }}{% endif %}{% else %}{% if let Some(topics) = understanding.topics %}{{ topics }}{% endif %}{% endif %}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if let Some(line) = social_line %}
|
{% if why.is_some() || understanding.interests.is_some() %}
|
||||||
<p class="social">{{ line }}</p>
|
<div class="why">
|
||||||
|
{% if let Some(text) = why %}
|
||||||
|
<p class="why-line"><em>Why it's here: {{ text }}</em></p>
|
||||||
|
{% endif %}
|
||||||
|
{% if let Some(interests) = understanding.interests %}
|
||||||
|
<p class="why-matches">Matches: {{ interests }}</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if let Some(text) = summary %}
|
{% if let Some(text) = summary %}
|
||||||
<p class="summary">{{ text }}</p>
|
<p class="summary">{{ text }}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if let Some(line) = social_line %}
|
||||||
|
<p class="social">{{ line }}</p>
|
||||||
|
{% endif %}
|
||||||
{% if excerpt_only %}
|
{% if excerpt_only %}
|
||||||
<p class="notice">(excerpt only — read online)</p>
|
<p class="notice">(excerpt only — read online)</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -26,9 +36,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<hr class="rule"/>
|
<hr class="rule"/>
|
||||||
<div class="article-footer">
|
<div class="article-footer">
|
||||||
{% if let Some(line) = understanding %}
|
|
||||||
<p class="understood">{{ line }}</p>
|
|
||||||
{% endif %}
|
|
||||||
{% if let Some(links) = rating %}
|
{% if let Some(links) = rating %}
|
||||||
<p class="rating">Was this a good pick?   <a href="{{ links.loved_url }}">[ Loved it ]</a>   <a href="{{ links.good_url }}">[ Good ]</a>   <a href="{{ links.not_for_me_url }}">[ Not for me ]</a>   <a href="{{ links.slop_url }}">[ AI slop ]</a>     <a href="{{ read_online_url }}">Read online ↗</a></p>
|
<p class="rating">Was this a good pick?   <a href="{{ links.loved_url }}">[ Loved it ]</a>   <a href="{{ links.good_url }}">[ Good ]</a>   <a href="{{ links.not_for_me_url }}">[ Not for me ]</a>   <a href="{{ links.slop_url }}">[ AI slop ]</a>     <a href="{{ read_online_url }}">Read online ↗</a></p>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|||||||
@@ -10,14 +10,21 @@
|
|||||||
<li class="index-entry">
|
<li class="index-entry">
|
||||||
<p class="index-title"><a href="{{ entry.href }}">{{ entry.title }}</a></p>
|
<p class="index-title"><a href="{{ entry.href }}">{{ entry.title }}</a></p>
|
||||||
<p class="index-meta">{{ entry.source }} · {{ entry.reading_minutes }} min read</p>
|
<p class="index-meta">{{ entry.source }} · {{ entry.reading_minutes }} min read</p>
|
||||||
|
{% if entry.understanding.kicker.is_some() || entry.understanding.topics.is_some() %}
|
||||||
|
<p class="rubric">{% if let Some(kicker) = entry.understanding.kicker %}<span class="kicker">{{ kicker }}</span>{% if let Some(topics) = entry.understanding.topics %}   {{ topics }}{% endif %}{% else %}{% if let Some(topics) = entry.understanding.topics %}{{ topics }}{% endif %}{% endif %}</p>
|
||||||
|
{% endif %}
|
||||||
|
{% if entry.why.is_some() || entry.understanding.interests.is_some() %}
|
||||||
|
<div class="index-why">
|
||||||
|
{% if let Some(text) = entry.why %}
|
||||||
|
<p class="why-line"><em>Why it's here: {{ text }}</em></p>
|
||||||
|
{% endif %}
|
||||||
|
{% if let Some(interests) = entry.understanding.interests %}
|
||||||
|
<p class="why-matches">Matches: {{ interests }}</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
{% if !entry.summary.is_empty() %}
|
{% if !entry.summary.is_empty() %}
|
||||||
<p class="index-summary">{{ entry.summary }}</p>
|
<p class="index-summary">{{ entry.summary }}</p>
|
||||||
{% endif %}
|
|
||||||
{% if let Some(text) = entry.why %}
|
|
||||||
<p class="index-why"><em>Why it's here: {{ text }}</em></p>
|
|
||||||
{% endif %}
|
|
||||||
{% if let Some(line) = entry.understanding %}
|
|
||||||
<p class="index-understood">{{ line }}</p>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
@@ -78,8 +78,7 @@ img {
|
|||||||
.stats,
|
.stats,
|
||||||
.meta,
|
.meta,
|
||||||
.social,
|
.social,
|
||||||
.understood,
|
.rubric,
|
||||||
.index-understood,
|
|
||||||
.index-meta,
|
.index-meta,
|
||||||
.discussion-note,
|
.discussion-note,
|
||||||
.attribution,
|
.attribution,
|
||||||
@@ -87,8 +86,29 @@ img {
|
|||||||
font-size: 0.85em;
|
font-size: 0.85em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.understood {
|
.rubric {
|
||||||
margin: 0 0 0.5em 0;
|
margin: 0;
|
||||||
|
color: #444444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kicker {
|
||||||
|
font-variant: small-caps;
|
||||||
|
}
|
||||||
|
|
||||||
|
.why,
|
||||||
|
.index-why {
|
||||||
|
margin: 0.2em 0 0 0;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.why-line {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.why-matches {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.85em;
|
||||||
|
color: #444444;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dateline,
|
.dateline,
|
||||||
@@ -128,8 +148,7 @@ ul.index-list {
|
|||||||
|
|
||||||
.index-title,
|
.index-title,
|
||||||
.index-meta,
|
.index-meta,
|
||||||
.index-summary,
|
.index-summary {
|
||||||
.index-understood {
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,5 +216,3 @@ p.comment-line {
|
|||||||
.fact-line {
|
.fact-line {
|
||||||
margin: 0 0 0.35em 0;
|
margin: 0 0 0.35em 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.why, .index-why { font-size: 0.9em; font-style: italic; }
|
|
||||||
|
|||||||
@@ -154,14 +154,26 @@ hr.rule {
|
|||||||
font-variant: small-caps;
|
font-variant: small-caps;
|
||||||
}
|
}
|
||||||
|
|
||||||
.understood, .index-understood {
|
.rubric {
|
||||||
margin: 0 0 0.75em 0;
|
margin: 0.2em 0 0 0;
|
||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
color: #444444;
|
color: #444444;
|
||||||
}
|
}
|
||||||
|
|
||||||
.index-understood {
|
.why,
|
||||||
margin: 0.2em 0 0 0;
|
.index-why {
|
||||||
|
margin: 0.35em 0 0 0;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.why-line {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.why-matches {
|
||||||
|
margin: 0.1em 0 0 0;
|
||||||
|
font-size: 0.85em;
|
||||||
|
color: #444444;
|
||||||
}
|
}
|
||||||
|
|
||||||
.summary {
|
.summary {
|
||||||
@@ -306,5 +318,3 @@ blockquote.comment blockquote.comment {
|
|||||||
.fact-line {
|
.fact-line {
|
||||||
margin: 0 0 0.35em 0;
|
margin: 0 0 0.35em 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.why, .index-why { font-size: 0.9em; font-style: italic; }
|
|
||||||
|
|||||||
+16
-5
@@ -882,7 +882,7 @@ struct FullEntry {
|
|||||||
is_lead: bool,
|
is_lead: bool,
|
||||||
summary: String,
|
summary: String,
|
||||||
why: Option<String>,
|
why: Option<String>,
|
||||||
understanding: Option<String>,
|
understanding: chapters::Understanding,
|
||||||
rating: Option<RatingWidget>,
|
rating: Option<RatingWidget>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -952,7 +952,7 @@ struct ArticleTemplate {
|
|||||||
meta_line: String,
|
meta_line: String,
|
||||||
why: Option<String>,
|
why: Option<String>,
|
||||||
social_line: Option<String>,
|
social_line: Option<String>,
|
||||||
understanding: Option<String>,
|
understanding: chapters::Understanding,
|
||||||
summary: Option<String>,
|
summary: Option<String>,
|
||||||
excerpt_only: bool,
|
excerpt_only: bool,
|
||||||
body_html: String,
|
body_html: String,
|
||||||
@@ -1030,7 +1030,7 @@ pub async fn render_full(
|
|||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.to_string(),
|
.to_string(),
|
||||||
why: pick.why.clone(),
|
why: pick.why.clone(),
|
||||||
understanding: chapters::understanding_line(pick),
|
understanding: chapters::understanding(pick),
|
||||||
rating: is_admin.then(|| {
|
rating: is_admin.then(|| {
|
||||||
RatingWidget::for_issue(
|
RatingWidget::for_issue(
|
||||||
pick.article.id,
|
pick.article.id,
|
||||||
@@ -1134,7 +1134,7 @@ pub async fn article(
|
|||||||
),
|
),
|
||||||
why: pick.why.clone(),
|
why: pick.why.clone(),
|
||||||
social_line: chapters::social_line(&article.social),
|
social_line: chapters::social_line(&article.social),
|
||||||
understanding: chapters::understanding_line(pick),
|
understanding: chapters::understanding(pick),
|
||||||
summary: summary_for(&view.issue, pick).map(str::to_string),
|
summary: summary_for(&view.issue, pick).map(str::to_string),
|
||||||
excerpt_only: article.excerpt_only,
|
excerpt_only: article.excerpt_only,
|
||||||
body_html: prepare_body(&article.content_html),
|
body_html: prepare_body(&article.content_html),
|
||||||
@@ -2001,7 +2001,9 @@ mod tests {
|
|||||||
assert!(issue.contains("What it argues"));
|
assert!(issue.contains("What it argues"));
|
||||||
assert!(issue.contains("A short abstract for the second piece"));
|
assert!(issue.contains("A short abstract for the second piece"));
|
||||||
assert!(issue.contains("Why it"));
|
assert!(issue.contains("Why it"));
|
||||||
assert!(issue.contains("Interests: Filesystems, Rust"));
|
assert!(issue.contains("Software engineering · Analysis"));
|
||||||
|
assert!(issue.contains("copy-on-write · ZFS"));
|
||||||
|
assert!(issue.contains("Matches: Filesystems · Rust"));
|
||||||
assert!(issue.contains("A. Writer · Example Feed"));
|
assert!(issue.contains("A. Writer · Example Feed"));
|
||||||
assert!(!issue.contains("example feed · Example Feed"));
|
assert!(!issue.contains("example feed · Example Feed"));
|
||||||
assert!(issue.contains("World Briefing"));
|
assert!(issue.contains("World Briefing"));
|
||||||
@@ -2031,6 +2033,15 @@ mod tests {
|
|||||||
assert!(article.contains("The write path is the interesting part"));
|
assert!(article.contains("The write path is the interesting part"));
|
||||||
assert!(article.contains("loading=\"lazy\""));
|
assert!(article.contains("loading=\"lazy\""));
|
||||||
assert!(article.contains("referrerpolicy=\"no-referrer\""));
|
assert!(article.contains("referrerpolicy=\"no-referrer\""));
|
||||||
|
let rubric_position = article.find("Software engineering · Analysis").unwrap();
|
||||||
|
let why_position = article.find("Why it's here").unwrap();
|
||||||
|
let summary_position = article
|
||||||
|
.find("What it argues, and why it is worth the time.")
|
||||||
|
.unwrap();
|
||||||
|
let social_position = article.find("342 on HN").unwrap();
|
||||||
|
assert!(rubric_position < why_position);
|
||||||
|
assert!(why_position < summary_position);
|
||||||
|
assert!(summary_position < social_position);
|
||||||
assert!(article.contains("A Niche Delight"));
|
assert!(article.contains("A Niche Delight"));
|
||||||
assert!(article.contains("rel=\"next\""));
|
assert!(article.contains("rel=\"next\""));
|
||||||
assert!(!article.contains(&dashboard_href));
|
assert!(!article.contains(&dashboard_href));
|
||||||
|
|||||||
+14
-3
@@ -54,7 +54,7 @@ pub struct PublicEntry {
|
|||||||
pub word_count: i64,
|
pub word_count: i64,
|
||||||
pub summary: Option<String>,
|
pub summary: Option<String>,
|
||||||
pub why: Option<String>,
|
pub why: Option<String>,
|
||||||
pub understanding: Option<String>,
|
pub understanding: crate::epub::chapters::Understanding,
|
||||||
pub comment_links: Vec<CommentLink>,
|
pub comment_links: Vec<CommentLink>,
|
||||||
pub is_lead: bool,
|
pub is_lead: bool,
|
||||||
}
|
}
|
||||||
@@ -133,7 +133,7 @@ impl From<&Issue> for PublicIssue {
|
|||||||
.filter(|summary| !summary.is_empty())
|
.filter(|summary| !summary.is_empty())
|
||||||
.map(str::to_string),
|
.map(str::to_string),
|
||||||
why: pick.why.clone(),
|
why: pick.why.clone(),
|
||||||
understanding: crate::epub::chapters::understanding_line(pick),
|
understanding: crate::epub::chapters::understanding(pick),
|
||||||
comment_links,
|
comment_links,
|
||||||
is_lead: pick.is_lead,
|
is_lead: pick.is_lead,
|
||||||
}
|
}
|
||||||
@@ -444,7 +444,18 @@ mod tests {
|
|||||||
assert!(html.contains("A short abstract for the second piece."));
|
assert!(html.contains("A short abstract for the second piece."));
|
||||||
assert!(html.contains("The systems story with enough operational detail to matter"));
|
assert!(html.contains("The systems story with enough operational detail to matter"));
|
||||||
assert!(html.contains("A small-scene delight outside the usual technical orbit"));
|
assert!(html.contains("A small-scene delight outside the usual technical orbit"));
|
||||||
assert!(html.contains("Interests: Filesystems, Rust"));
|
assert!(html.contains("Software engineering · Analysis"));
|
||||||
|
assert!(html.contains("copy-on-write · ZFS"));
|
||||||
|
assert!(html.contains("Matches: Filesystems · Rust"));
|
||||||
|
let rubric_position = html.find("Software engineering · Analysis").unwrap();
|
||||||
|
let why_position = html.find("Why it's here").unwrap();
|
||||||
|
let summary_position = html
|
||||||
|
.find("What it argues, and why it is worth the time.")
|
||||||
|
.unwrap();
|
||||||
|
let comments_position = html.find(">Hacker News:").unwrap();
|
||||||
|
assert!(rubric_position < why_position);
|
||||||
|
assert!(why_position < summary_position);
|
||||||
|
assert!(summary_position < comments_position);
|
||||||
for private in [
|
for private in [
|
||||||
"Two stories today",
|
"Two stories today",
|
||||||
"Body of",
|
"Body of",
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
|||||||
|
{% if understanding.kicker.is_some() || understanding.topics.is_some() %}<p class="mt-3 font-sans text-sm text-muted">{% if let Some(kicker) = understanding.kicker %}<span class="text-[0.72rem] uppercase tracking-[0.12em]">{{ kicker }}</span>{% if let Some(topics) = understanding.topics %}<span class="ml-3">{{ topics }}</span>{% endif %}{% else %}{% if let Some(topics) = understanding.topics %}<span>{{ topics }}</span>{% endif %}{% endif %}</p>{% endif %}{% if why.is_some() || understanding.interests.is_some() %}<div class="mt-4 border-l-2 border-accent pl-3">{% if let Some(why) = why %}<p class="italic text-ink-2">Why it's here: {{ why }}</p>{% endif %}{% if let Some(interests) = understanding.interests %}<p class="mt-1 font-sans text-sm text-muted">Matches: {{ interests }}</p>{% endif %}</div>{% endif %}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{% extends "layout.html" %}{% block ears %}<span class="sm:hidden">{{ toc.short_date }}</span><span class="hidden sm:inline">{{ toc.display_date }} · No. {{ toc.issue_number }}</span>{% endblock %}{% block content %}<div class="mx-auto max-w-7xl lg:grid lg:grid-cols-[15rem_minmax(0,1fr)] lg:gap-x-8 lg:px-6 xl:grid-cols-[18rem_minmax(0,1fr)] xl:gap-x-14">{% include "_toc.html" %}
|
{% extends "layout.html" %}{% block ears %}<span class="sm:hidden">{{ toc.short_date }}</span><span class="hidden sm:inline">{{ toc.display_date }} · No. {{ toc.issue_number }}</span>{% endblock %}{% block content %}<div class="mx-auto max-w-7xl lg:grid lg:grid-cols-[15rem_minmax(0,1fr)] lg:gap-x-8 lg:px-6 xl:grid-cols-[18rem_minmax(0,1fr)] xl:gap-x-14">{% include "_toc.html" %}
|
||||||
<article class="reader-page mx-auto mt-10 w-full max-w-[68ch] px-4 sm:px-6 lg:mt-12 lg:px-0" data-toc-scroll>
|
<article class="reader-page mx-auto mt-10 w-full max-w-[68ch] px-4 sm:px-6 lg:mt-12 lg:px-0" data-toc-scroll>
|
||||||
<header><p class="font-sans text-[0.72rem] uppercase tracking-[0.12em] text-muted">Article</p><h1 class="mt-3 text-4xl font-semibold leading-[1.1] tracking-[-0.01em] sm:text-5xl"><a class="text-ink no-underline hover:text-accent" href="{{ source_url }}">{{ title }}</a></h1>{% match byline %}{% when Some with (byline) %}<p class="mt-5 font-sans text-sm font-medium text-ink-2">{{ byline }}</p>{% when None %}{% endmatch %}<p class="mt-1 font-sans text-sm text-muted">{% include "_source.html" %} · {{ meta_line }}</p>{% match why %}{% when Some with (why) %}<p class="mt-6 border-l-2 border-accent pl-3 italic text-ink-2">Why it's here: {{ why }}</p>{% when None %}{% endmatch %}{% match understanding %}{% when Some with (line) %}<p class="mt-4 font-sans text-sm text-muted">{{ line }}</p>{% when None %}{% endmatch %}{% match social_line %}{% when Some with (social) %}<p class="mt-4 font-sans text-sm text-muted">{{ social }}</p>{% when None %}{% endmatch %}{% match summary %}{% when Some with (summary) %}<p class="mt-7 text-xl italic leading-relaxed text-ink-2">{{ summary }}</p>{% when None %}{% endmatch %}{% if excerpt_only %}<p class="notice mt-6">Excerpt only — continue reading at the original site.</p>{% endif %}</header>
|
<header><p class="font-sans text-[0.72rem] uppercase tracking-[0.12em] text-muted">Article</p><h1 class="mt-3 text-4xl font-semibold leading-[1.1] tracking-[-0.01em] sm:text-5xl"><a class="text-ink no-underline hover:text-accent" href="{{ source_url }}">{{ title }}</a></h1>{% match byline %}{% when Some with (byline) %}<p class="mt-5 font-sans text-sm font-medium text-ink-2">{{ byline }}</p>{% when None %}{% endmatch %}<p class="mt-1 font-sans text-sm text-muted">{% include "_source.html" %} · {{ meta_line }}</p>{% include "_understanding.html" %}{% match summary %}{% when Some with (summary) %}<p class="mt-7 text-xl italic leading-relaxed text-ink-2">{{ summary }}</p>{% when None %}{% endmatch %}{% match social_line %}{% when Some with (social) %}<p class="mt-4 font-sans text-sm text-muted">{{ social }}</p>{% when None %}{% endmatch %}{% if excerpt_only %}<p class="notice mt-6">Excerpt only — continue reading at the original site.</p>{% endif %}</header>
|
||||||
<div class="prose-body mt-10 border-t border-rule pt-8">{{ body_html|safe }}</div>
|
<div class="prose-body mt-10 border-t border-rule pt-8">{{ body_html|safe }}</div>
|
||||||
{% match discussion_html %}{% when Some with (discussion) %}<section class="discussion prose-body mt-12 border-t border-rule pt-6 [&_.comment-meta]:font-sans [&_.comment-meta]:text-sm [&_.comment-meta]:text-muted [&_blockquote.reply]:ml-6"><h2 class="mb-6 border-0 pt-0 text-2xl">Discussion</h2>{{ discussion|safe }}</section>{% when None %}{% endmatch %}
|
{% match discussion_html %}{% when Some with (discussion) %}<section class="discussion prose-body mt-12 border-t border-rule pt-6 [&_.comment-meta]:font-sans [&_.comment-meta]:text-sm [&_.comment-meta]:text-muted [&_blockquote.reply]:ml-6"><h2 class="mb-6 border-0 pt-0 text-2xl">Discussion</h2>{{ discussion|safe }}</section>{% when None %}{% endmatch %}
|
||||||
<footer class="mt-12 border-t border-rule pt-6">{% match rating %}{% when Some with (widget) %}{% include "_rating_widget.html" %}{% when None %}{% endmatch %}<p class="mt-6 flex flex-wrap gap-2"><a class="btn" href="{{ read_online_url }}">Read online ↗</a>{% if page.is_admin() %}<a class="btn" href="{{ dashboard_href }}">Dashboard</a>{% endif %}</p><nav class="mt-10 grid grid-cols-1 gap-3 font-sans sm:grid-cols-2" aria-label="Adjacent articles">{% match previous %}{% when Some with (previous) %}<a class="group min-h-24 border border-rule p-4 text-ink no-underline hover:border-accent" rel="prev" href="{{ previous.href }}"><span class="block text-[0.68rem] uppercase tracking-[0.12em] text-muted">Previous</span><span class="mt-2 block leading-snug group-hover:text-accent">← {{ previous.title }}</span></a>{% when None %}{% endmatch %}{% match next %}{% when Some with (next) %}<a class="group min-h-24 border border-rule p-4 text-right text-ink no-underline hover:border-accent sm:col-start-2" rel="next" href="{{ next.href }}"><span class="block text-[0.68rem] uppercase tracking-[0.12em] text-muted">Next</span><span class="mt-2 block leading-snug group-hover:text-accent">{{ next.title }} →</span></a>{% when None %}{% endmatch %}</nav><p class="mt-8 font-sans text-sm"><a href="{{ issue_href }}">← Back to this issue</a></p></footer>
|
<footer class="mt-12 border-t border-rule pt-6">{% match rating %}{% when Some with (widget) %}{% include "_rating_widget.html" %}{% when None %}{% endmatch %}<p class="mt-6 flex flex-wrap gap-2"><a class="btn" href="{{ read_online_url }}">Read online ↗</a>{% if page.is_admin() %}<a class="btn" href="{{ dashboard_href }}">Dashboard</a>{% endif %}</p><nav class="mt-10 grid grid-cols-1 gap-3 font-sans sm:grid-cols-2" aria-label="Adjacent articles">{% match previous %}{% when Some with (previous) %}<a class="group min-h-24 border border-rule p-4 text-ink no-underline hover:border-accent" rel="prev" href="{{ previous.href }}"><span class="block text-[0.68rem] uppercase tracking-[0.12em] text-muted">Previous</span><span class="mt-2 block leading-snug group-hover:text-accent">← {{ previous.title }}</span></a>{% when None %}{% endmatch %}{% match next %}{% when Some with (next) %}<a class="group min-h-24 border border-rule p-4 text-right text-ink no-underline hover:border-accent sm:col-start-2" rel="next" href="{{ next.href }}"><span class="block text-[0.68rem] uppercase tracking-[0.12em] text-muted">Next</span><span class="mt-2 block leading-snug group-hover:text-accent">{{ next.title }} →</span></a>{% when None %}{% endmatch %}</nav><p class="mt-8 font-sans text-sm"><a href="{{ issue_href }}">← Back to this issue</a></p></footer>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<section aria-labelledby="brief-heading" data-toc-entry="/issues/{{ date }}"><h1 id="brief-heading" class="reader-section-heading">The Brief</h1><div class="editorial prose-body mt-5 [&>p:first-child]:first-letter:float-left [&>p:first-child]:first-letter:mr-2 [&>p:first-child]:first-letter:mt-1 [&>p:first-child]:first-letter:font-serif [&>p:first-child]:first-letter:text-[4.6rem] [&>p:first-child]:first-letter:font-semibold [&>p:first-child]:first-letter:leading-[0.72]">{{ front_page_html|safe }}</div></section>
|
<section aria-labelledby="brief-heading" data-toc-entry="/issues/{{ date }}"><h1 id="brief-heading" class="reader-section-heading">The Brief</h1><div class="editorial prose-body mt-5 [&>p:first-child]:first-letter:float-left [&>p:first-child]:first-letter:mr-2 [&>p:first-child]:first-letter:mt-1 [&>p:first-child]:first-letter:font-serif [&>p:first-child]:first-letter:text-[4.6rem] [&>p:first-child]:first-letter:font-semibold [&>p:first-child]:first-letter:leading-[0.72]">{{ front_page_html|safe }}</div></section>
|
||||||
{% if (page.is_admin() && read_href.is_some()) || downloads.is_some() %}<div class="my-8 flex flex-wrap gap-2 border-y border-rule py-4">{% if page.is_admin() %}{% match read_href %}{% when Some with (href) %}<a class="btn" href="{{ href }}" target="_blank" rel="noopener">Read in BookOrbit</a>{% when None %}{% endmatch %}{% endif %}{% match downloads %}{% when Some with (downloads) %}{% if downloads.others.is_empty() %}<a class="btn" href="{{ downloads.primary.href }}">Download {{ downloads.primary.label }}</a>{% else %}<div class="relative inline-flex"><a class="btn rounded-r-none" href="{{ downloads.primary.href }}">Download {{ downloads.primary.label }}</a><details class="-ml-px"><summary class="btn list-none rounded-l-none px-2 [&::-webkit-details-marker]:hidden" aria-label="Choose download format"><span aria-hidden="true">▾</span></summary><div class="absolute left-0 top-full z-20 mt-1 min-w-52 border border-rule bg-paper p-1 shadow-[0_18px_32px_-26px_rgb(0_0_0_/_0.6)]"><a class="btn group w-full justify-between border-0" href="{{ downloads.primary.href }}"><span>{% if downloads.primary.label == "EPUB" %}Standard {% endif %}{{ downloads.primary.label }}</span><span class="ml-4 text-muted group-hover:text-paper">{{ downloads.primary.size }}</span></a>{% for download in downloads.others %}<a class="btn group w-full justify-between border-0" href="{{ download.href }}"><span>{{ download.label }}</span><span class="ml-4 text-muted group-hover:text-paper">{{ download.size }}</span></a>{% endfor %}</div></details></div>{% endif %}{% when None %}{% endmatch %}</div>{% endif %}
|
{% if (page.is_admin() && read_href.is_some()) || downloads.is_some() %}<div class="my-8 flex flex-wrap gap-2 border-y border-rule py-4">{% if page.is_admin() %}{% match read_href %}{% when Some with (href) %}<a class="btn" href="{{ href }}" target="_blank" rel="noopener">Read in BookOrbit</a>{% when None %}{% endmatch %}{% endif %}{% match downloads %}{% when Some with (downloads) %}{% if downloads.others.is_empty() %}<a class="btn" href="{{ downloads.primary.href }}">Download {{ downloads.primary.label }}</a>{% else %}<div class="relative inline-flex"><a class="btn rounded-r-none" href="{{ downloads.primary.href }}">Download {{ downloads.primary.label }}</a><details class="-ml-px"><summary class="btn list-none rounded-l-none px-2 [&::-webkit-details-marker]:hidden" aria-label="Choose download format"><span aria-hidden="true">▾</span></summary><div class="absolute left-0 top-full z-20 mt-1 min-w-52 border border-rule bg-paper p-1 shadow-[0_18px_32px_-26px_rgb(0_0_0_/_0.6)]"><a class="btn group w-full justify-between border-0" href="{{ downloads.primary.href }}"><span>{% if downloads.primary.label == "EPUB" %}Standard {% endif %}{{ downloads.primary.label }}</span><span class="ml-4 text-muted group-hover:text-paper">{{ downloads.primary.size }}</span></a>{% for download in downloads.others %}<a class="btn group w-full justify-between border-0" href="{{ download.href }}"><span>{{ download.label }}</span><span class="ml-4 text-muted group-hover:text-paper">{{ download.size }}</span></a>{% endfor %}</div></details></div>{% endif %}{% when None %}{% endmatch %}</div>{% endif %}
|
||||||
<section class="mt-12" aria-labelledby="index-heading"><h2 id="index-heading" class="text-center text-3xl font-semibold leading-[1.1] tracking-[-0.01em]">In This Issue</h2>
|
<section class="mt-12" aria-labelledby="index-heading"><h2 id="index-heading" class="text-center text-3xl font-semibold leading-[1.1] tracking-[-0.01em]">In This Issue</h2>
|
||||||
{% for section in sections %}<section class="mt-14"><h3 class="reader-section-heading">{{ section.name }}</h3><ul class="m-0 list-none p-0">{% for entry in section.entries %}<li class="border-b border-rule py-6" data-toc-entry="{{ entry.href }}"><h4 class="font-serif font-semibold leading-[1.1] tracking-[-0.01em] {% if entry.is_lead %}text-3xl{% else %}text-2xl{% endif %}"><a class="text-ink no-underline hover:text-accent" href="{{ entry.href }}">{{ entry.title }}</a></h4><p class="mt-2 font-sans text-sm text-muted">{% match entry.author %}{% when Some with (author) %}{{ author }} · {% when None %}{% endmatch %}{% let source = entry.source %}{% include "_source.html" %} · {{ entry.reading_minutes }} min read{% if page.is_admin() %} · <a class="text-muted hover:text-accent" href="{{ entry.dashboard_href }}">dashboard</a>{% endif %}</p>{% if !entry.summary.is_empty() %}<p class="index-summary mt-4">{{ entry.summary }}</p>{% endif %}{% match entry.why %}{% when Some with (why) %}<p class="mt-4 border-l-2 border-accent pl-3 italic text-ink-2">Why it's here: {{ why }}</p>{% when None %}{% endmatch %}{% match entry.understanding %}{% when Some with (line) %}<p class="mt-3 font-sans text-sm text-muted">{{ line }}</p>{% when None %}{% endmatch %}{% match entry.rating %}{% when Some with (widget) %}{% include "_rating_widget.html" %}{% when None %}{% endmatch %}</li>{% endfor %}</ul></section>{% endfor %}
|
{% for section in sections %}<section class="mt-14"><h3 class="reader-section-heading">{{ section.name }}</h3><ul class="m-0 list-none p-0">{% for entry in section.entries %}<li class="border-b border-rule py-6" data-toc-entry="{{ entry.href }}"><h4 class="font-serif font-semibold leading-[1.1] tracking-[-0.01em] {% if entry.is_lead %}text-3xl{% else %}text-2xl{% endif %}"><a class="text-ink no-underline hover:text-accent" href="{{ entry.href }}">{{ entry.title }}</a></h4><p class="mt-2 font-sans text-sm text-muted">{% match entry.author %}{% when Some with (author) %}{{ author }} · {% when None %}{% endmatch %}{% let source = entry.source %}{% include "_source.html" %} · {{ entry.reading_minutes }} min read{% if page.is_admin() %} · <a class="text-muted hover:text-accent" href="{{ entry.dashboard_href }}">dashboard</a>{% endif %}</p>{% let understanding = entry.understanding %}{% let why = entry.why %}{% include "_understanding.html" %}{% if !entry.summary.is_empty() %}<p class="index-summary mt-4">{{ entry.summary }}</p>{% endif %}{% match entry.rating %}{% when Some with (widget) %}{% include "_rating_widget.html" %}{% when None %}{% endmatch %}</li>{% endfor %}</ul></section>{% endfor %}
|
||||||
</section>
|
</section>
|
||||||
{% if has_world || has_behind %}<nav class="my-12 flex flex-wrap items-center justify-center gap-x-3 gap-y-1 border-y border-rule py-4 text-center font-sans text-sm uppercase tracking-[0.08em]" aria-label="Issue chapters">{% if has_world %}<a class="text-ink no-underline hover:text-accent" href="/issues/{{ date }}/world">World Briefing</a>{% endif %}{% if has_world && has_behind %}<span class="text-muted" aria-hidden="true">·</span>{% endif %}{% if has_behind %}<a class="text-ink no-underline hover:text-accent" href="/issues/{{ date }}/behind">Behind the paper</a>{% endif %}</nav>{% endif %}
|
{% if has_world || has_behind %}<nav class="my-12 flex flex-wrap items-center justify-center gap-x-3 gap-y-1 border-y border-rule py-4 text-center font-sans text-sm uppercase tracking-[0.08em]" aria-label="Issue chapters">{% if has_world %}<a class="text-ink no-underline hover:text-accent" href="/issues/{{ date }}/world">World Briefing</a>{% endif %}{% if has_world && has_behind %}<span class="text-muted" aria-hidden="true">·</span>{% endif %}{% if has_behind %}<a class="text-ink no-underline hover:text-accent" href="/issues/{{ date }}/behind">Behind the paper</a>{% endif %}</nav>{% endif %}
|
||||||
<footer id="colophon" class="mt-14 scroll-mt-6 font-sans text-sm leading-relaxed" data-toc-entry="/issues/{{ date }}#colophon"><h2 class="reader-section-heading">Colophon</h2><p class="my-4 text-ink-2"><em>The Daily EPUB</em> is assembled every morning from a personal feed reader.</p><dl class="kv border-y border-rule py-4 text-xs sm:text-sm"><dt>Generated</dt><dd>{{ colophon.generated_at }}</dd><dt>Bulk model</dt><dd>{{ colophon.bulk_model }}</dd><dt>Editor model</dt><dd>{{ colophon.editor_model }}</dd><dt>Summaries model</dt><dd>{{ colophon.summaries_model }}</dd><dt>Entries considered</dt><dd>{% match colophon.entries_fetched %}{% when Some with (entries) %}{{ entries }}{% match colophon.feeds_seen %}{% when Some with (feeds) %} from {{ feeds }} feeds{% when None %}{% endmatch %}{% when None %}n/a{% endmatch %}</dd><dt>Candidates scored</dt><dd>{% match colophon.candidates %}{% when Some with (candidates) %}{{ candidates }}{% when None %}n/a{% endmatch %}</dd><dt>Articles selected</dt><dd>{{ colophon.article_count }} across {{ colophon.section_count }} sections</dd><dt>Words</dt><dd>{{ colophon.total_words }} · ~{{ colophon.reading_minutes }} min read</dd>{% for cost in colophon.provider_costs %}<dt>{{ cost.provider }} cost</dt><dd>{{ cost.cost }}</dd>{% endfor %}<dt>Total token cost</dt><dd>{% match colophon.cost_usd %}{% when Some with (cost) %}{{ cost }}{% when None %}n/a{% endmatch %}</dd><dt>Generator</dt><dd>{{ colophon.generator_version }}</dd></dl></footer>
|
<footer id="colophon" class="mt-14 scroll-mt-6 font-sans text-sm leading-relaxed" data-toc-entry="/issues/{{ date }}#colophon"><h2 class="reader-section-heading">Colophon</h2><p class="my-4 text-ink-2"><em>The Daily EPUB</em> is assembled every morning from a personal feed reader.</p><dl class="kv border-y border-rule py-4 text-xs sm:text-sm"><dt>Generated</dt><dd>{{ colophon.generated_at }}</dd><dt>Bulk model</dt><dd>{{ colophon.bulk_model }}</dd><dt>Editor model</dt><dd>{{ colophon.editor_model }}</dd><dt>Summaries model</dt><dd>{{ colophon.summaries_model }}</dd><dt>Entries considered</dt><dd>{% match colophon.entries_fetched %}{% when Some with (entries) %}{{ entries }}{% match colophon.feeds_seen %}{% when Some with (feeds) %} from {{ feeds }} feeds{% when None %}{% endmatch %}{% when None %}n/a{% endmatch %}</dd><dt>Candidates scored</dt><dd>{% match colophon.candidates %}{% when Some with (candidates) %}{{ candidates }}{% when None %}n/a{% endmatch %}</dd><dt>Articles selected</dt><dd>{{ colophon.article_count }} across {{ colophon.section_count }} sections</dd><dt>Words</dt><dd>{{ colophon.total_words }} · ~{{ colophon.reading_minutes }} min read</dd>{% for cost in colophon.provider_costs %}<dt>{{ cost.provider }} cost</dt><dd>{{ cost.cost }}</dd>{% endfor %}<dt>Total token cost</dt><dd>{% match colophon.cost_usd %}{% when Some with (cost) %}{{ cost }}{% when None %}n/a{% endmatch %}</dd><dt>Generator</dt><dd>{{ colophon.generator_version }}</dd></dl></footer>
|
||||||
|
|||||||
@@ -4,6 +4,6 @@
|
|||||||
<header class="mb-10 text-center"><p class="font-sans text-[0.72rem] uppercase tracking-[0.12em] text-muted">{{ issue.display_date }} · No. {{ issue.issue_number }}</p><p class="mt-2 font-sans text-sm text-muted">{{ issue.stats_line }}</p><p class="mx-auto mt-5 max-w-[58ch] text-lg italic leading-relaxed text-ink-2">A personal morning paper, assembled daily; the selection is the reader's, the words are the authors'.</p></header>
|
<header class="mb-10 text-center"><p class="font-sans text-[0.72rem] uppercase tracking-[0.12em] text-muted">{{ issue.display_date }} · No. {{ issue.issue_number }}</p><p class="mt-2 font-sans text-sm text-muted">{{ issue.stats_line }}</p><p class="mx-auto mt-5 max-w-[58ch] text-lg italic leading-relaxed text-ink-2">A personal morning paper, assembled daily; the selection is the reader's, the words are the authors'.</p></header>
|
||||||
<p class="notice mb-8"><a href="/login">Sign in</a> to read every article's full text and download the editions, or <a href="/request-access">request access</a>.</p>
|
<p class="notice mb-8"><a href="/login">Sign in</a> to read every article's full text and download the editions, or <a href="/request-access">request access</a>.</p>
|
||||||
{% if !downloads.is_empty() %}<div class="mb-8 flex flex-wrap justify-center gap-2">{% for download in downloads %}<a class="btn" href="{{ download.href }}">{{ download.label }} <span class="ml-1 text-muted">{{ download.size }}</span></a>{% endfor %}</div>{% endif %}
|
{% if !downloads.is_empty() %}<div class="mb-8 flex flex-wrap justify-center gap-2">{% for download in downloads %}<a class="btn" href="{{ download.href }}">{{ download.label }} <span class="ml-1 text-muted">{{ download.size }}</span></a>{% endfor %}</div>{% endif %}
|
||||||
{% for section in issue.sections %}<section class="mt-14"><h2 class="reader-section-heading">{{ section.name }}</h2><div>{% for entry in section.entries %}<article class="border-b border-rule py-6{% if entry.is_lead %} pt-5{% endif %}"><h3 class="font-semibold leading-[1.1] tracking-[-0.01em] {% if entry.is_lead %}text-3xl{% else %}text-2xl{% endif %}"><a class="text-ink no-underline hover:text-accent" href="{{ entry.url }}">{{ entry.title }}</a></h3><p class="mt-2 font-sans text-sm leading-relaxed text-muted">{% match entry.author %}{% when Some with (author) %}{{ author }} · {% when None %}{% endmatch %}{{ entry.source }}{% match entry.publication %}{% when Some with (publication) %} · {{ publication }}{% when None %}{% endmatch %} · {{ entry.reading_minutes }} min</p>{% match entry.summary %}{% when Some with (summary) %}<p class="index-summary mt-4">{{ summary }}</p>{% when None %}{% endmatch %}{% match entry.why %}{% when Some with (why) %}<p class="mt-4 border-l-2 border-accent pl-3 italic text-ink-2">Why it's here: {{ why }}</p>{% when None %}{% endmatch %}{% match entry.understanding %}{% when Some with (line) %}<p class="mt-3 font-sans text-sm text-muted">{{ line }}</p>{% when None %}{% endmatch %}{% if !entry.comment_links.is_empty() %}<p class="mt-3 flex flex-wrap gap-x-4 gap-y-1 font-sans text-sm text-muted">{% for link in entry.comment_links %}<a class="text-muted hover:text-accent" rel="noopener" target="_blank" href="{{ link.url }}">{{ link.label }}{% if !link.meta.is_empty() %}: {{ link.meta }}{% endif %}</a>{% endfor %}</p>{% endif %}</article>{% endfor %}</div></section>{% endfor %}
|
{% for section in issue.sections %}<section class="mt-14"><h2 class="reader-section-heading">{{ section.name }}</h2><div>{% for entry in section.entries %}<article class="border-b border-rule py-6{% if entry.is_lead %} pt-5{% endif %}"><h3 class="font-semibold leading-[1.1] tracking-[-0.01em] {% if entry.is_lead %}text-3xl{% else %}text-2xl{% endif %}"><a class="text-ink no-underline hover:text-accent" href="{{ entry.url }}">{{ entry.title }}</a></h3><p class="mt-2 font-sans text-sm leading-relaxed text-muted">{% match entry.author %}{% when Some with (author) %}{{ author }} · {% when None %}{% endmatch %}{{ entry.source }}{% match entry.publication %}{% when Some with (publication) %} · {{ publication }}{% when None %}{% endmatch %} · {{ entry.reading_minutes }} min</p>{% let understanding = entry.understanding %}{% let why = entry.why %}{% include "_understanding.html" %}{% match entry.summary %}{% when Some with (summary) %}<p class="index-summary mt-4">{{ summary }}</p>{% when None %}{% endmatch %}{% if !entry.comment_links.is_empty() %}<p class="mt-3 flex flex-wrap gap-x-4 gap-y-1 font-sans text-sm text-muted">{% for link in entry.comment_links %}<a class="text-muted hover:text-accent" rel="noopener" target="_blank" href="{{ link.url }}">{{ link.label }}{% if !link.meta.is_empty() %}: {{ link.meta }}{% endif %}</a>{% endfor %}</p>{% endif %}</article>{% endfor %}</div></section>{% endfor %}
|
||||||
<p class="mt-10 text-center font-sans text-sm"><a href="/issues">Browse the archive →</a></p>
|
<p class="mt-10 text-center font-sans text-sm"><a href="/issues">Browse the archive →</a></p>
|
||||||
{% endif %}</article>{% endblock %}
|
{% endif %}</article>{% endblock %}
|
||||||
|
|||||||
Reference in New Issue
Block a user