Repository navigation
fix(rss): hand the pipeline plain text instead of feed HTML - #151
Conversation
The next commit extracts an RSS entry's body text with it. Chosen over pydefuddle 0.1.0 (its only release), which on Substack pages returned the "Subscribe / Sign in" chrome or the footer instead of the post, and over Defuddle itself, which is JavaScript and would bring Node back into an image that dropped it on purpose (see the Dockerfile header). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Article.content of an RSS entry was the feed's raw HTML, and every prompt snippet (scoring, summarize, clustering, vote order) and language detection cut its first N characters. A post that opens with a captioned image spends thousands of characters on <picture srcset> markup: on the 2026-11-03 morning issue the Tidy First and ByteByteGo stories were summarized from image tags and a sponsor block, and across 234 entries from 51 feeds one in five got under 200 characters of text in a 1000-character window. Both feed adapters now pass the entry through html_text.plain_text: trafilatura extracts the body (tables kept, captions dropped); a fragment it declines, such as a one-line teaser, is tag-stripped with html.parser, not a regex, so prose like "x < 3" survives and a tag a feed cut off mid-way does not leak. On CPython 3.12 before 3.12.16 html.parser can assert on "<
Why
An RSS article's
Article.contentwas the feed's raw HTML, and every LLM prompt snippet (scoring, summarize, clustering, vote order) plus language detection cut its first N characters. A Substack-style post that opens with a captioned image spends thousands of characters on<picture srcset>markup before the first sentence.On the 2026-11-03 morning issue, two of the three Stories were summarized from that markup:
Across 234 entries from 51 feeds, 46 (one in five) got under 200 characters of text in a 1000-character window, mostly Substack and Ghost sources.
What
adapters/fetch/html_text.plain_textturns an entry's HTML into body text. trafilatura does the extraction, with tables kept and captions dropped. When trafilatura declines a fragment, such as a one-line teaser,html.parserstrips the tags. On CPython 3.12 before 3.12.16,html.parsercan assert, so that case falls back to a regex strip.CloudflareRssSourceandRssSourceboth call it, off the event loop, once per read.RssSourcestill reads the image from the raw HTML before converting.stored_articles.contentis written as plain text. Rows stored earlier keep their HTML, becauseINSERT OR IGNOREnever rewrites them.degrade.excerptgets a docstring-only change that states its known cost on plain text: a literalVec<String>loses<String>, and&is decoded twice.Rejected
<(Price < $5 and > $1becamePrice $1) and leaked tags a feed had cut off mid-way.Verification
scripts/check.sh: 4761 passed.<and>, drops a truncated tag, and drops a tag whose attribute contains>;a<bwith no later>drops the rest of the text;<script>text leaks from a single-element fragment, as it did before.After deploy
Only articles fetched after the deploy carry text, so a rerun of an old issue shows no change. Check the next issue's Substack and Ghost summaries.
🤖 Generated with Claude Code