Show matched interests as a tag row instead of a callout

With the why line gone the accent-bar callout held nothing but "Matches: a · b",
which no longer earned its quoting. Every surface now puts one row directly
under the rubric: a small-caps "Matches" label and one pill chip per interest.
On the web that is one `.interest-chip` component (admins still get links to
the interest's article filter); the EPUB gets `.matches`/`.interest` rules in
both stylesheets, and `Understanding::interests_line` goes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ESJbbzQ1b8fs5AA65tKw2q
This commit is contained in:
2026-09-15 01:09:14 +00:00
co-authored by Claude Fable 5.1
parent 36424784af
commit ce9b629115
10 changed files with 71 additions and 47 deletions
+29 -15
View File
@@ -239,17 +239,6 @@ pub struct InterestRef {
pub href: String, pub href: String,
} }
impl Understanding {
/// EPUBs keep interest names as plain text, without dashboard links.
pub fn interests_line(&self) -> String {
self.interests
.iter()
.map(|interest| interest.name.as_str())
.collect::<Vec<_>>()
.join(" \u{00b7} ")
}
}
/// Reader-facing assessment details, split so templates can give each part the /// Reader-facing assessment details, split so templates can give each part the
/// same editorial hierarchy across web and EPUB surfaces. /// same editorial hierarchy across web and EPUB surfaces.
pub fn understanding(pick: &Pick) -> Understanding { pub fn understanding(pick: &Pick) -> Understanding {
@@ -919,7 +908,21 @@ mod tests {
// The lead's rubric and matches are on the index; the second pick has neither. // 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_eq!(chapter.xhtml.matches("class=\"rubric\"").count(), 1);
assert!(chapter.xhtml.contains("Software engineering · Analysis")); assert!(chapter.xhtml.contains("Software engineering · Analysis"));
assert!(chapter.xhtml.contains("Matches: Filesystems · Rust")); assert!(
chapter
.xhtml
.contains("<span class=\"interest\">Filesystems</span>")
);
assert!(
chapter
.xhtml
.contains("<span class=\"interest\">Rust</span>")
);
let rubric_position = chapter.xhtml.find("class=\"rubric\"").unwrap();
let matches_position = chapter.xhtml.find("class=\"matches\"").unwrap();
let summary_position = chapter.xhtml.find("class=\"index-summary\"").unwrap();
assert!(rubric_position < matches_position);
assert!(matches_position < summary_position);
// 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 &#38; Other Tales")); assert!(chapter.xhtml.contains("A Niche Delight &#38; Other Tales"));
assert_xml_ok(&chapter.xhtml); assert_xml_ok(&chapter.xhtml);
@@ -992,11 +995,22 @@ mod tests {
assert!(chapter.xhtml.contains("class=\"rubric\"")); assert!(chapter.xhtml.contains("class=\"rubric\""));
assert!(chapter.xhtml.contains("Software engineering · Analysis")); assert!(chapter.xhtml.contains("Software engineering · Analysis"));
assert!(chapter.xhtml.contains("copy-on-write · ZFS")); assert!(chapter.xhtml.contains("copy-on-write · ZFS"));
assert!(chapter.xhtml.contains("Matches: Filesystems · Rust")); assert!(
chapter
.xhtml
.contains("<span class=\"interest\">Filesystems</span>")
);
assert!(
chapter
.xhtml
.contains("<span class=\"interest\">Rust</span>")
);
let rubric_position = chapter.xhtml.find("class=\"rubric\"").unwrap(); let rubric_position = chapter.xhtml.find("class=\"rubric\"").unwrap();
let matches_position = chapter.xhtml.find("class=\"matches\"").unwrap();
let summary_position = chapter.xhtml.find("class=\"summary\"").unwrap(); let summary_position = chapter.xhtml.find("class=\"summary\"").unwrap();
let social_position = chapter.xhtml.find("class=\"social\"").unwrap(); let social_position = chapter.xhtml.find("class=\"social\"").unwrap();
assert!(rubric_position < summary_position); assert!(rubric_position < matches_position);
assert!(matches_position < summary_position);
assert!(summary_position < social_position); assert!(summary_position < social_position);
let footer = chapter let footer = chapter
.xhtml .xhtml
@@ -1004,7 +1018,7 @@ mod tests {
.unwrap() .unwrap()
.1; .1;
assert!(!footer.contains("class=\"rubric\"")); assert!(!footer.contains("class=\"rubric\""));
assert!(!footer.contains("Matches:")); assert!(!footer.contains("class=\"matches\""));
let second = render_article( let second = render_article(
&issue, &issue,
&issue.lineup.picks[1], &issue.lineup.picks[1],
+1 -5
View File
@@ -11,11 +11,7 @@
<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> <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 %} {% endif %}
{% if !understanding.interests.is_empty() %} {% if !understanding.interests.is_empty() %}
<div class="why"> <p class="matches"><span class="kicker">Matches</span>{% for interest in understanding.interests %} <span class="interest">{{ interest.name }}</span>{% endfor %}</p>
{% if !understanding.interests.is_empty() %}
<p class="why-matches">Matches: {{ understanding.interests_line() }}</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>
+1 -5
View File
@@ -14,11 +14,7 @@
<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> <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 %} {% endif %}
{% if !entry.understanding.interests.is_empty() %} {% if !entry.understanding.interests.is_empty() %}
<div class="index-why"> <p class="matches"><span class="kicker">Matches</span>{% for interest in entry.understanding.interests %} <span class="interest">{{ interest.name }}</span>{% endfor %}</p>
{% if !entry.understanding.interests.is_empty() %}
<p class="why-matches">Matches: {{ entry.understanding.interests_line() }}</p>
{% endif %}
</div>
{% endif %} {% 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>
+11 -8
View File
@@ -95,16 +95,19 @@ img {
font-variant: small-caps; font-variant: small-caps;
} }
.why, .matches {
.index-why { margin: 0.35em 0 0 0;
margin: 0.2em 0 0 0; font-size: 0.8em;
font-size: 0.9em; color: #444444;
} }
.why-matches { .interest {
margin: 0; display: inline-block;
font-size: 0.85em; margin: 0 0.15em 0.15em 0;
color: #444444; padding: 0 0.5em;
border: 1px solid #888888;
border-radius: 0.8em;
white-space: nowrap;
} }
.dateline, .dateline,
+10 -7
View File
@@ -160,16 +160,19 @@ hr.rule {
color: #444444; color: #444444;
} }
.why, .matches {
.index-why {
margin: 0.35em 0 0 0; margin: 0.35em 0 0 0;
font-size: 0.9em; font-size: 0.8em;
color: #444444;
} }
.why-matches { .interest {
margin: 0.1em 0 0 0; display: inline-block;
font-size: 0.85em; margin: 0 0.15em 0.15em 0;
color: #444444; padding: 0 0.5em;
border: 1px solid #888888;
border-radius: 0.8em;
white-space: nowrap;
} }
.summary { .summary {
+12 -3
View File
@@ -1994,7 +1994,13 @@ mod tests {
assert!(issue.contains("A short abstract for the second piece")); assert!(issue.contains("A short abstract for the second piece"));
assert!(issue.contains("Software engineering · Analysis")); assert!(issue.contains("Software engineering · Analysis"));
assert!(issue.contains("copy-on-write · ZFS")); assert!(issue.contains("copy-on-write · ZFS"));
assert!(issue.contains("Matches: Filesystems · Rust")); assert!(issue.contains("<span class=\"interest-chip\">Filesystems</span>"));
assert!(issue.contains("<span class=\"interest-chip\">Rust</span>"));
let issue_rubric_position = issue.find("Software engineering · Analysis").unwrap();
let issue_matches_position = issue.find(">Matches</span>").unwrap();
let issue_summary_position = issue.find("What it argues").unwrap();
assert!(issue_rubric_position < issue_matches_position);
assert!(issue_matches_position < issue_summary_position);
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"));
@@ -2027,7 +2033,9 @@ mod tests {
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 rubric_position = article.find("Software engineering · Analysis").unwrap();
let matches_position = article.find("Matches: Filesystems · Rust").unwrap(); let matches_position = article.find(">Matches</span>").unwrap();
assert!(article.contains("<span class=\"interest-chip\">Filesystems</span>"));
assert!(article.contains("<span class=\"interest-chip\">Rust</span>"));
let summary_position = article let summary_position = article
.find("What it argues, and why it is worth the time.") .find("What it argues, and why it is worth the time.")
.unwrap(); .unwrap();
@@ -2734,7 +2742,8 @@ mod tests {
assert!(admin_issue.contains("Was this a good pick?")); assert!(admin_issue.contains("Was this a good pick?"));
assert!(admin_issue.contains("value=\"loved\" data-label=\"loved\" class=\"active\"")); assert!(admin_issue.contains("value=\"loved\" data-label=\"loved\" class=\"active\""));
assert!(admin_issue.contains(&dashboard_href)); assert!(admin_issue.contains(&dashboard_href));
assert!(admin_issue.contains(&crate::interests::articles_href("Filesystems"))); let interest_href = crate::interests::articles_href("Filesystems");
assert!(admin_issue.contains(&format!("class=\"interest-chip\" href=\"{interest_href}\"")));
let admin_article = app let admin_article = app
.clone() .clone()
+3 -2
View File
@@ -444,9 +444,10 @@ mod tests {
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("Software engineering · Analysis")); assert!(html.contains("Software engineering · Analysis"));
assert!(html.contains("copy-on-write · ZFS")); assert!(html.contains("copy-on-write · ZFS"));
assert!(html.contains("Matches: Filesystems · Rust")); assert!(html.contains("<span class=\"interest-chip\">Filesystems</span>"));
assert!(html.contains("<span class=\"interest-chip\">Rust</span>"));
let rubric_position = html.find("Software engineering · Analysis").unwrap(); let rubric_position = html.find("Software engineering · Analysis").unwrap();
let matches_position = html.find("Matches: Filesystems · Rust").unwrap(); let matches_position = html.find(">Matches</span>").unwrap();
let summary_position = html let summary_position = html
.find("What it argues, and why it is worth the time.") .find("What it argues, and why it is worth the time.")
.unwrap(); .unwrap();
File diff suppressed because one or more lines are too long
+2
View File
@@ -186,6 +186,8 @@
.reader-page :where(h1, h2, h3, h4), .reader-section-heading { text-wrap:balance; } .reader-page :where(h1, h2, h3, h4), .reader-section-heading { text-wrap:balance; }
.reader-section-heading { @apply border-t-[3px] border-double border-rule-strong pt-3 font-serif text-[1.05rem] font-semibold uppercase leading-[1.2] tracking-[0.15em] text-ink; } .reader-section-heading { @apply border-t-[3px] border-double border-rule-strong pt-3 font-serif text-[1.05rem] font-semibold uppercase leading-[1.2] tracking-[0.15em] text-ink; }
.interest-chip { @apply inline-block rounded-full border border-rule-strong px-2.5 py-0.5 text-xs leading-5 text-ink-2 no-underline; }
a.interest-chip:hover { @apply border-accent text-accent; }
.rating { @apply my-5 flex flex-wrap items-center gap-x-3 font-sans text-xs; } .rating { @apply my-5 flex flex-wrap items-center gap-x-3 font-sans text-xs; }
.rating-prompt { @apply font-serif text-[0.8rem] italic text-muted; } .rating-prompt { @apply font-serif text-[0.8rem] italic text-muted; }
+1 -1
View File
@@ -1 +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 !understanding.interests.is_empty() %}<div class="mt-4 border-l-2 border-accent pl-3">{% if !understanding.interests.is_empty() %}<p class="mt-1 font-sans text-sm text-muted">Matches: {% for interest in understanding.interests %}{% if page.is_admin() %}<a href="{{ interest.href }}">{{ interest.name }}</a>{% else %}{{ interest.name }}{% endif %}{% if !loop.last %} · {% endif %}{% endfor %}</p>{% endif %}</div>{% endif %} {% 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 !understanding.interests.is_empty() %}<p class="mt-2.5 flex flex-wrap items-center gap-1.5 font-sans"><span class="mr-1 text-[0.72rem] uppercase tracking-[0.12em] text-muted">Matches</span>{% for interest in understanding.interests %}{% if page.is_admin() %}<a class="interest-chip" href="{{ interest.href }}">{{ interest.name }}</a>{% else %}<span class="interest-chip">{{ interest.name }}</span>{% endif %}{% endfor %}</p>{% endif %}