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,
}
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
/// same editorial hierarchy across web and EPUB surfaces.
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.
assert_eq!(chapter.xhtml.matches("class=\"rubric\"").count(), 1);
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.
assert!(chapter.xhtml.contains("A Niche Delight &#38; Other Tales"));
assert_xml_ok(&chapter.xhtml);
@@ -992,11 +995,22 @@ mod tests {
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"));
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=\"summary\"").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);
let footer = chapter
.xhtml
@@ -1004,7 +1018,7 @@ mod tests {
.unwrap()
.1;
assert!(!footer.contains("class=\"rubric\""));
assert!(!footer.contains("Matches:"));
assert!(!footer.contains("class=\"matches\""));
let second = render_article(
&issue,
&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>
{% endif %}
{% if !understanding.interests.is_empty() %}
<div class="why">
{% if !understanding.interests.is_empty() %}
<p class="why-matches">Matches: {{ understanding.interests_line() }}</p>
{% endif %}
</div>
<p class="matches"><span class="kicker">Matches</span>{% for interest in understanding.interests %} <span class="interest">{{ interest.name }}</span>{% endfor %}</p>
{% endif %}
{% if let Some(text) = summary %}
<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>
{% endif %}
{% if !entry.understanding.interests.is_empty() %}
<div class="index-why">
{% if !entry.understanding.interests.is_empty() %}
<p class="why-matches">Matches: {{ entry.understanding.interests_line() }}</p>
{% endif %}
</div>
<p class="matches"><span class="kicker">Matches</span>{% for interest in entry.understanding.interests %} <span class="interest">{{ interest.name }}</span>{% endfor %}</p>
{% endif %}
{% if !entry.summary.is_empty() %}
<p class="index-summary">{{ entry.summary }}</p>
+11 -8
View File
@@ -95,16 +95,19 @@ img {
font-variant: small-caps;
}
.why,
.index-why {
margin: 0.2em 0 0 0;
font-size: 0.9em;
.matches {
margin: 0.35em 0 0 0;
font-size: 0.8em;
color: #444444;
}
.why-matches {
margin: 0;
font-size: 0.85em;
color: #444444;
.interest {
display: inline-block;
margin: 0 0.15em 0.15em 0;
padding: 0 0.5em;
border: 1px solid #888888;
border-radius: 0.8em;
white-space: nowrap;
}
.dateline,
+10 -7
View File
@@ -160,16 +160,19 @@ hr.rule {
color: #444444;
}
.why,
.index-why {
.matches {
margin: 0.35em 0 0 0;
font-size: 0.9em;
font-size: 0.8em;
color: #444444;
}
.why-matches {
margin: 0.1em 0 0 0;
font-size: 0.85em;
color: #444444;
.interest {
display: inline-block;
margin: 0 0.15em 0.15em 0;
padding: 0 0.5em;
border: 1px solid #888888;
border-radius: 0.8em;
white-space: nowrap;
}
.summary {
+12 -3
View File
@@ -1994,7 +1994,13 @@ mod tests {
assert!(issue.contains("A short abstract for the second piece"));
assert!(issue.contains("Software engineering · Analysis"));
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("example feed · Example Feed"));
assert!(issue.contains("World Briefing"));
@@ -2027,7 +2033,9 @@ mod tests {
assert!(article.contains("loading=\"lazy\""));
assert!(article.contains("referrerpolicy=\"no-referrer\""));
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
.find("What it argues, and why it is worth the time.")
.unwrap();
@@ -2734,7 +2742,8 @@ mod tests {
assert!(admin_issue.contains("Was this a good pick?"));
assert!(admin_issue.contains("value=\"loved\" data-label=\"loved\" class=\"active\""));
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
.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("Software engineering · Analysis"));
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 matches_position = html.find("Matches: Filesystems · Rust").unwrap();
let matches_position = html.find(">Matches</span>").unwrap();
let summary_position = html
.find("What it argues, and why it is worth the time.")
.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-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-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 %}