One article header everywhere: rubric, why block, then the summary

The web index, the article page and both EPUB pages each ordered the same
metadata differently, and the "understanding" line was one wall of text
with the why line above it on one page and below it on another. Every
surface now reads title, meta, rubric, why block, summary:

- the rubric is a newspaper kicker (topic group and format in small caps)
  followed by the specific topics; depth and technicality stay on the
  dashboard article page only
- the accent-bar why block holds the editor's line and "Matches: …" (the
  matched interests) beneath it
- the social line moves after the summary on the article page and chapter,
  matching the index pages, and the EPUB chapter footer no longer carries
  the understanding line

`understanding_line` becomes an `Understanding { kicker, topics, interests }`
struct shared by the web view models and the EPUB, and the three web
templates include one `_understanding.html` partial.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NxctvrEPQZ9pX58LRgGYNZ
This commit is contained in:
2026-09-12 04:50:48 +00:00
co-authored by Claude Fable 5.1
parent 71079a93f5
commit 4338dc1a99
12 changed files with 191 additions and 115 deletions
+86 -74
View File
@@ -43,7 +43,7 @@ struct IndexEntry {
reading_minutes: i64,
summary: String,
why: Option<String>,
understanding: Option<String>,
understanding: Understanding,
}
struct IndexSection {
@@ -81,7 +81,7 @@ struct ArticleChapter {
byline: Option<String>,
meta_line: String,
social_line: Option<String>,
understanding: Option<String>,
understanding: Understanding,
why: Option<String>,
summary: Option<String>,
excerpt_only: bool,
@@ -209,67 +209,61 @@ fn facet_label(token: &str) -> Option<String> {
"boston_new_england" => "Boston & New England",
"outdoors_lifestyle" => "Outdoors & lifestyle",
"history" => "History",
"reported_news" => "reported news",
"analysis_essay" => "analysis essay",
"how_to_technical" => "how-to",
"first_hand_account" => "first-hand account",
"announcement_roundup" => "announcement",
"code_repository" => "code repository",
"documentation_reference" => "documentation",
"tool_or_product_page" => "product page",
"discussion_thread" => "discussion thread",
"paper_or_report" => "paper or report",
"interview_or_transcript" => "interview",
"video_or_podcast" => "video or podcast",
"fiction_or_humor" => "fiction or humor",
"brief" => "brief",
"standard" => "standard depth",
"deep" => "in depth",
"nontechnical" => "non-technical",
"light" => "lightly technical",
"intermediate" => "moderately technical",
"advanced" => "highly technical",
"reported_news" => "Reported",
"analysis_essay" => "Analysis",
"how_to_technical" => "How-to",
"first_hand_account" => "First person",
"announcement_roundup" => "Announcement",
"code_repository" => "Code",
"documentation_reference" => "Documentation",
"tool_or_product_page" => "Product page",
"discussion_thread" => "Discussion",
"paper_or_report" => "Paper",
"interview_or_transcript" => "Interview",
"video_or_podcast" => "Video or podcast",
"fiction_or_humor" => "Fiction & humor",
"other" => return None,
unknown => return Some(unknown.replace('_', " ")),
};
Some(label.to_string())
}
/// One muted line saying what the pipeline understood about an article: the
/// deep-assessment facets, the extracted topics and the best-matching reader
/// interests. `None` when there is nothing to say (no deep read, no interests).
pub fn understanding_line(pick: &Pick) -> Option<String> {
let mut parts = Vec::new();
if let Some(deep) = &pick.llm {
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct Understanding {
pub kicker: Option<String>,
pub topics: Option<String>,
pub interests: Option<String>,
}
/// 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;
for token in [
facets.topic_group.as_deref(),
facets.format.as_deref(),
facets.depth.as_deref(),
facets.technicality.as_deref(),
]
let kicker = [facets.topic_group.as_deref(), facets.format.as_deref()]
.into_iter()
.flatten()
{
if let Some(label) = facet_label(token).filter(|label| !label.is_empty()) {
parts.push(label);
}
}
if let Some(topics) = &facets.specific_topics {
.filter_map(facet_label)
.filter(|label| !label.is_empty())
.collect::<Vec<_>>();
let kicker = (!kicker.is_empty()).then(|| kicker.join(" \u{00b7} "));
let topics = facets.specific_topics.as_ref().and_then(|topics| {
let topics = topics
.iter()
.map(|topic| topic.trim())
.filter(|topic| !topic.is_empty())
.collect::<Vec<_>>();
if !topics.is_empty() {
parts.push(format!("Topics: {}", topics.join(", ")));
(!topics.is_empty()).then(|| topics.join(" \u{00b7} "))
});
(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 {
@@ -394,7 +388,7 @@ pub fn render_in_this_issue(issue: &Issue) -> Result<Chapter, EpubError> {
reading_minutes: pick.article.reading_minutes(),
summary: summary_for(issue, pick).unwrap_or_default().to_string(),
why: pick.why.clone(),
understanding: understanding_line(pick),
understanding: understanding(pick),
})
.collect();
sections.push(IndexSection { name, entries });
@@ -409,7 +403,7 @@ pub fn render_in_this_issue(issue: &Issue) -> Result<Chapter, EpubError> {
reading_minutes: 3,
summary: "The day's events, as recorded by the Current Events portal.".into(),
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}")),
meta_line: meta_parts.join(" \u{00b7} "),
social_line: social_line(&article.social),
understanding: understanding_line(pick),
understanding: understanding(pick),
why: pick.why.clone(),
summary: summary_for(issue, pick).map(str::to_string),
excerpt_only: article.excerpt_only,
@@ -810,35 +804,43 @@ mod tests {
}
#[test]
fn understanding_line_includes_facets_topics_and_interests() {
fn understanding_includes_kicker_topics_and_interests() {
let issue = issue();
assert_eq!(
understanding_line(&issue.lineup.picks[0]).as_deref(),
Some(
"Software engineering · analysis essay · in depth · highly technical · Topics: copy-on-write, ZFS · Interests: Filesystems, Rust"
)
understanding(&issue.lineup.picks[0]),
Understanding {
kicker: Some("Software engineering · Analysis".into()),
topics: Some("copy-on-write · ZFS".into()),
interests: Some("Filesystems · Rust".into()),
}
);
}
#[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();
assert!(understanding_line(&issue.lineup.picks[1]).is_none());
assert_eq!(
understanding(&issue.lineup.picks[1]),
Understanding::default()
);
}
#[test]
fn understanding_line_can_contain_only_interests() {
fn understanding_can_contain_only_interests() {
let issue = issue();
let mut pick = issue.lineup.picks[1].clone();
pick.top_interests = vec!["Rust".into()];
assert_eq!(
understanding_line(&pick).as_deref(),
Some("Interests: Rust")
understanding(&pick),
Understanding {
interests: Some("Rust".into()),
..Understanding::default()
}
);
}
#[test]
fn understanding_line_skips_other_topic_group() {
fn understanding_skips_other_topic_group() {
let issue = issue();
let mut pick = issue.lineup.picks[0].clone();
pick.llm.as_mut().unwrap().facets = crate::types::Facets {
@@ -846,7 +848,7 @@ mod tests {
..crate::types::Facets::default()
};
pick.top_interests.clear();
assert!(understanding_line(&pick).is_none());
assert_eq!(understanding(&pick), Understanding::default());
}
#[test]
@@ -883,12 +885,10 @@ mod tests {
assert!(chapter.xhtml.contains("6 min read"));
assert!(chapter.xhtml.contains("What it argues"));
assert!(chapter.xhtml.contains("A short abstract"));
// The lead's understanding line is on the index; the second pick has none.
assert_eq!(
chapter.xhtml.matches("class=\"index-understood\"").count(),
1
);
assert!(chapter.xhtml.contains("Interests: Filesystems, Rust"));
// The lead's rubric and matches are on the index; the second pick has neither.
assert_eq!(chapter.xhtml.matches("class=\"rubric\"").count(), 1);
assert!(chapter.xhtml.contains("Software engineering · Analysis"));
assert!(chapter.xhtml.contains("Matches: Filesystems · Rust"));
// Titles are escaped (askama emits numeric references), never injected raw.
assert!(chapter.xhtml.contains("A Niche Delight &#38; Other Tales"));
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("Read online"));
assert!(chapter.xhtml.contains("href=\"disc-1001.xhtml\""));
assert!(chapter.xhtml.contains("class=\"understood\""));
assert!(chapter.xhtml.contains(
"Software engineering · analysis essay · in depth · highly technical · Topics: copy-on-write, ZFS · Interests: Filesystems, Rust"
));
assert!(chapter.xhtml.contains("class=\"rubric\""));
assert!(chapter.xhtml.contains("Software engineering · Analysis"));
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(
&issue,
&issue.lineup.picks[1],
@@ -971,7 +983,7 @@ mod tests {
Some("s3cret"),
)
.unwrap();
assert!(!second.xhtml.contains("class=\"understood\""));
assert!(!second.xhtml.contains("class=\"rubric\""));
// The un-downloaded image degrades to a placeholder.
assert!(
chapter
+14 -7
View File
@@ -7,15 +7,25 @@
<p class="byline">{{ line }}</p>
{% endif %}
<p class="meta">{{ meta_line }}</p>
{% if let Some(text) = why %}
<p class="why"><em>Why it&#39;s here: {{ text }}</em></p>
{% if understanding.kicker.is_some() || understanding.topics.is_some() %}
<p class="rubric">{% if let Some(kicker) = understanding.kicker %}<span class="kicker">{{ kicker }}</span>{% if let Some(topics) = understanding.topics %} &#160; {{ topics }}{% endif %}{% else %}{% if let Some(topics) = understanding.topics %}{{ topics }}{% endif %}{% endif %}</p>
{% endif %}
{% if let Some(line) = social_line %}
<p class="social">{{ line }}</p>
{% if why.is_some() || understanding.interests.is_some() %}
<div class="why">
{% if let Some(text) = why %}
<p class="why-line"><em>Why it&#39;s here: {{ text }}</em></p>
{% endif %}
{% if let Some(interests) = understanding.interests %}
<p class="why-matches">Matches: {{ interests }}</p>
{% endif %}
</div>
{% endif %}
{% if let Some(text) = summary %}
<p class="summary">{{ text }}</p>
{% endif %}
{% if let Some(line) = social_line %}
<p class="social">{{ line }}</p>
{% endif %}
{% if excerpt_only %}
<p class="notice">(excerpt only &#8212; read online)</p>
{% endif %}
@@ -26,9 +36,6 @@
</div>
<hr class="rule"/>
<div class="article-footer">
{% if let Some(line) = understanding %}
<p class="understood">{{ line }}</p>
{% endif %}
{% if let Some(links) = rating %}
<p class="rating">Was this a good pick? &#160; <a href="{{ links.loved_url }}">[ Loved it ]</a> &#160; <a href="{{ links.good_url }}">[ Good ]</a> &#160; <a href="{{ links.not_for_me_url }}">[ Not for me ]</a> &#160; <a href="{{ links.slop_url }}">[ AI slop ]</a> &#160;&#160;&#160; <a href="{{ read_online_url }}">Read online &#8599;</a></p>
{% else %}
+13 -6
View File
@@ -10,14 +10,21 @@
<li class="index-entry">
<p class="index-title"><a href="{{ entry.href }}">{{ entry.title }}</a></p>
<p class="index-meta">{{ entry.source }} &#183; {{ 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 %} &#160; {{ 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&#39;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() %}
<p class="index-summary">{{ entry.summary }}</p>
{% endif %}
{% if let Some(text) = entry.why %}
<p class="index-why"><em>Why it&#39;s here: {{ text }}</em></p>
{% endif %}
{% if let Some(line) = entry.understanding %}
<p class="index-understood">{{ line }}</p>
{% endif %}
</li>
{% endfor %}
+25 -8
View File
@@ -78,8 +78,7 @@ img {
.stats,
.meta,
.social,
.understood,
.index-understood,
.rubric,
.index-meta,
.discussion-note,
.attribution,
@@ -87,8 +86,29 @@ img {
font-size: 0.85em;
}
.understood {
margin: 0 0 0.5em 0;
.rubric {
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,
@@ -128,8 +148,7 @@ ul.index-list {
.index-title,
.index-meta,
.index-summary,
.index-understood {
.index-summary {
margin: 0;
}
@@ -197,5 +216,3 @@ p.comment-line {
.fact-line {
margin: 0 0 0.35em 0;
}
.why, .index-why { font-size: 0.9em; font-style: italic; }
+16 -6
View File
@@ -154,14 +154,26 @@ hr.rule {
font-variant: small-caps;
}
.understood, .index-understood {
margin: 0 0 0.75em 0;
.rubric {
margin: 0.2em 0 0 0;
font-size: 0.8em;
color: #444444;
}
.index-understood {
margin: 0.2em 0 0 0;
.why,
.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 {
@@ -306,5 +318,3 @@ blockquote.comment blockquote.comment {
.fact-line {
margin: 0 0 0.35em 0;
}
.why, .index-why { font-size: 0.9em; font-style: italic; }
+16 -5
View File
@@ -882,7 +882,7 @@ struct FullEntry {
is_lead: bool,
summary: String,
why: Option<String>,
understanding: Option<String>,
understanding: chapters::Understanding,
rating: Option<RatingWidget>,
}
@@ -952,7 +952,7 @@ struct ArticleTemplate {
meta_line: String,
why: Option<String>,
social_line: Option<String>,
understanding: Option<String>,
understanding: chapters::Understanding,
summary: Option<String>,
excerpt_only: bool,
body_html: String,
@@ -1030,7 +1030,7 @@ pub async fn render_full(
.unwrap_or_default()
.to_string(),
why: pick.why.clone(),
understanding: chapters::understanding_line(pick),
understanding: chapters::understanding(pick),
rating: is_admin.then(|| {
RatingWidget::for_issue(
pick.article.id,
@@ -1134,7 +1134,7 @@ pub async fn article(
),
why: pick.why.clone(),
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),
excerpt_only: article.excerpt_only,
body_html: prepare_body(&article.content_html),
@@ -2001,7 +2001,9 @@ mod tests {
assert!(issue.contains("What it argues"));
assert!(issue.contains("A short abstract for the second piece"));
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("example feed · Example Feed"));
assert!(issue.contains("World Briefing"));
@@ -2031,6 +2033,15 @@ mod tests {
assert!(article.contains("The write path is the interesting part"));
assert!(article.contains("loading=\"lazy\""));
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("rel=\"next\""));
assert!(!article.contains(&dashboard_href));
+14 -3
View File
@@ -54,7 +54,7 @@ pub struct PublicEntry {
pub word_count: i64,
pub summary: Option<String>,
pub why: Option<String>,
pub understanding: Option<String>,
pub understanding: crate::epub::chapters::Understanding,
pub comment_links: Vec<CommentLink>,
pub is_lead: bool,
}
@@ -133,7 +133,7 @@ impl From<&Issue> for PublicIssue {
.filter(|summary| !summary.is_empty())
.map(str::to_string),
why: pick.why.clone(),
understanding: crate::epub::chapters::understanding_line(pick),
understanding: crate::epub::chapters::understanding(pick),
comment_links,
is_lead: pick.is_lead,
}
@@ -444,7 +444,18 @@ mod tests {
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("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 [
"Two stories today",
"Body of",
File diff suppressed because one or more lines are too long
+1
View File
@@ -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 -1
View File
@@ -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" %}
<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>
{% 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>
+1 -1
View File
@@ -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>
{% 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>
{% 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>
{% 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>
+1 -1
View File
@@ -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>
<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 %}
{% 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>
{% endif %}</article>{% endblock %}