Take the author from the article page, not the aggregator submitter

Readability already finds a byline (JSON-LD author, meta author tags,
byline markup); keep it on the article and let it replace an aggregator
entry's author, which for HN and friends is the submitter. A direct feed's
own author is still trusted over the page, and an aggregator name is only
used at all when no direct feed carried the story. The author is stored on
articles so it survives independently of the best entry.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QVPagF6jfDv78CC5Jv2wp4
This commit is contained in:
2026-09-07 18:34:21 +00:00
co-authored by Claude Fable 5.1
parent 41c144bf20
commit 7ce8c65939
6 changed files with 181 additions and 15 deletions
+41
View File
@@ -380,8 +380,17 @@ fn build_article(members: Vec<(Entry, String)>, feed_urls: &FeedUrls) -> Article
}
});
// An aggregator entry's author is usually the submitter, so it only counts
// when no direct feed carried the story; extraction may still replace it
// with the page's own byline.
let is_direct = |e: &Entry| {
classify_source_with_feed(e, feed_urls.get(&e.feed_id).map(String::as_str))
== SourceKind::Feed
};
let has_direct = members.iter().any(|(e, _)| is_direct(e));
let author = members
.iter()
.filter(|(e, _)| !has_direct || is_direct(e))
.filter_map(|(e, _)| e.author.clone())
.find(|a| !a.trim().is_empty());
@@ -638,6 +647,38 @@ mod tests {
assert_eq!(articles[1].sources.len(), 1);
}
#[test]
fn direct_feed_author_beats_aggregator_submitter() {
let mut aggregator = entry(1, "https://blog.dev/post", "A Distinct Article Title");
aggregator.author = Some("HN Submitter".into());
aggregator.raw_content = format!("<p>{}</p>", "word ".repeat(50));
let mut direct = entry(2, "https://blog.dev/post", "A Distinct Article Title");
direct.author = Some("Real Writer".into());
let feed_urls = FeedUrls::from([
(aggregator.feed_id, "https://hnrss.org/frontpage".into()),
(direct.feed_id, "https://blog.dev/feed.xml".into()),
]);
let (articles, _) = cluster_with_feeds(vec![aggregator, direct], &feed_urls);
assert_eq!(articles.len(), 1);
assert_eq!(articles[0].best_entry_id, 1);
assert_eq!(articles[0].author.as_deref(), Some("Real Writer"));
// With a direct feed present, a submitter name is not used as a fallback.
let mut aggregator = entry(3, "https://blog.dev/other", "Another Distinct Title");
aggregator.author = Some("HN Submitter".into());
let mut direct = entry(4, "https://blog.dev/other", "Another Distinct Title");
direct.author = None;
let feed_urls = FeedUrls::from([
(aggregator.feed_id, "https://hnrss.org/frontpage".into()),
(direct.feed_id, "https://blog.dev/feed.xml".into()),
]);
let (articles, _) = cluster_with_feeds(vec![aggregator, direct], &feed_urls);
assert_eq!(articles[0].author, None);
}
#[test]
fn clustering_drops_non_articles_and_keeps_short_titles_apart() {
let mut a = entry(1, "https://a.dev/1", "News");