Simplify related articles: shared article header, ids only, no job

- Both article pages render one `_article_head.html` partial from an
  `ArticleHead` built by a single constructor instead of two copies.
- `related::for_article` returns ids; the cosine stays stored for tuning.
- Title keys travel with each candidate, dropping the side map.
- Drop the `related-refresh` dashboard job: the backfill is a one-off and
  `features related` covers it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AeJmv3WED4w8S3GV7mMjS7
This commit is contained in:
thalladaandClaude Opus 5.5 committed 2026-10-12 16:11:43 +00:00
1 parent 03f0a454be
commit 08b15c42b4
7 files changed
+79 -143

No files matched your search

+45 -49
View File
@@ -943,19 +943,8 @@ struct ArticleLink {
struct ArticleTemplate {
page: Page,
toc: Toc,
title: String,
source_url: String,
byline: Option<String>,
source: Source,
meta_line: String,
social_line: Option<String>,
understanding: chapters::Understanding,
summary: Option<String>,
excerpt_only: bool,
body_html: String,
head: ArticleHead,
discussion_html: Option<String>,
read_online_url: String,
dashboard_href: String,
rating: Option<RatingWidget>,
previous: Option<ArticleLink>,
next: Option<ArticleLink>,
@@ -969,18 +958,50 @@ struct ArticleTemplate {
#[template(path = "article_standalone.html")]
struct StandaloneArticleTemplate {
page: Page,
head: ArticleHead,
}
/// What `_article_head.html` renders (header and body) plus the footer links
/// both article pages share.
struct ArticleHead {
title: String,
source_url: String,
byline: Option<String>,
source: Source,
meta_line: String,
social_line: Option<String>,
/// Issue-only: empty and `None` on the standalone page.
understanding: chapters::Understanding,
summary: Option<String>,
excerpt_only: bool,
body_html: String,
read_online_url: String,
dashboard_href: String,
}
impl ArticleHead {
fn new(article: &Article, config: &Config, is_admin: bool) -> Self {
Self {
title: article.title.clone(),
source_url: article.canonical_url.clone(),
byline: article.author.as_ref().map(|author| format!("By {author}")),
source: Source::new(article, config, is_admin),
meta_line: format!(
"{} words · ~{} min read",
thousands(article.word_count),
article.reading_minutes()
),
social_line: chapters::social_line(&article.social),
understanding: chapters::Understanding::default(),
summary: None,
excerpt_only: article.excerpt_only,
body_html: prepare_body(&article.content_html),
read_online_url: article.url.clone(),
dashboard_href: format!("/dashboard/articles/{}", article.id),
}
}
}
/// One entry of the "Related" list under an issue article.
#[derive(Debug)]
struct RelatedView {
@@ -1150,26 +1171,15 @@ pub async fn article(
Ok(Html(ArticleTemplate {
page,
toc,
title: article.title.clone(),
source_url: article.canonical_url.clone(),
byline: article.author.as_ref().map(|author| format!("By {author}")),
source: Source::new(article, &config, is_admin),
meta_line: format!(
"{} words · ~{} min read",
thousands(article.word_count),
article.reading_minutes()
),
social_line: chapters::social_line(&article.social),
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),
head: ArticleHead {
understanding: chapters::understanding(pick),
summary: summary_for(&view.issue, pick).map(str::to_string),
..ArticleHead::new(article, &config, is_admin)
},
discussion_html: pick
.discussion
.as_ref()
.map(|discussion| crate::comments::render_xhtml(discussion, &article.title)),
read_online_url: article.url.clone(),
dashboard_href: format!("/dashboard/articles/{}", article.id),
rating: is_admin.then(|| {
RatingWidget::for_issue(
article.id,
@@ -1217,20 +1227,7 @@ pub async fn standalone(
page.flash = take_flash(&session).await?;
Ok(Html(StandaloneArticleTemplate {
page,
title: article.title.clone(),
source_url: article.canonical_url.clone(),
byline: article.author.as_ref().map(|author| format!("By {author}")),
source: Source::new(&article, &config, is_admin),
meta_line: format!(
"{} words · ~{} min read",
thousands(article.word_count),
article.reading_minutes()
),
social_line: chapters::social_line(&article.social),
excerpt_only: article.excerpt_only,
body_html: prepare_body(&article.content_html),
read_online_url: article.url.clone(),
dashboard_href: format!("/dashboard/articles/{}", article.id),
head: ArticleHead::new(&article, &config, is_admin),
})
.into_response())
}
@@ -1244,18 +1241,17 @@ async fn related_views(
date: Date,
article_id: ArticleId,
) -> anyhow::Result<Vec<RelatedView>> {
let links = crate::related::for_article(db, date, article_id).await?;
if links.is_empty() {
let ids = crate::related::for_article(db, date, article_id).await?;
if ids.is_empty() {
return Ok(Vec::new());
}
let ids: Vec<ArticleId> = links.iter().map(|link| link.article_id).collect();
let articles = db.get_articles(&ids).await?;
let issues = crate::related::latest_issues(db, &ids).await?;
Ok(links
Ok(ids
.iter()
.filter_map(|link| {
let article = articles.get(&link.article_id)?;
let href = match issues.get(&link.article_id) {
.filter_map(|id| {
let article = articles.get(id)?;
let href = match issues.get(id) {
Some(date) => article_href(*date, article.id),
None => standalone_href(article.id),
};