fix(#44): recover split article bodies, and add select.content to the recipe engine - #45
Merged
Merged
Conversation
Some CMS templates wedge an in-article call-to-action between two separate body containers. Readability scores a single top candidate and keeps only that candidate plus its direct siblings, so the entire lead section was dropped while the output still looked well-formed (#44). Ship a recipe that unwraps the two intervening wrapper layers, making both body containers siblings again. Verified end to end against the live page: 7.8k -> 10.5k chars, lead section and its heading restored, links and images intact. The Trafilatura auto-pick could not rescue this: its output carried the full text but no markdown headings, and pickBest requires at least one heading before preferring the longer candidate. The regression fixture is the real page's DOM skeleton with the prose replaced by same-length filler -- the structure triggers the bug, the wording does not. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF9FFG9xYHpyFexQQpe8CJ
The recipe engine was purely subtractive: select.remove and preprocess say what to throw away, but never what the article IS, so the final choice stayed with Readability's candidate scoring. That is exactly what fails when a body is split across containers (#44) -- no amount of removal helps, because the problem is not a surplus element, it is that half the article was never selected. select.content takes CSS selectors, joins every match in document order into one document, and uses that as the body, skipping both Readability scoring and the Trafilatura auto-pick. Nested matches collapse to the outermost so `[".a", ".a p"]` cannot emit prose twice; invalid selectors skip themselves; select.remove still applies first, so the two compose. Output carries source: recipe-content. A stale content selector would yield an empty article, so a selection under 200 chars falls back to the normal pipeline and records why in metadata.extractorReason -- a site redesign degrades the recipe to generic behavior instead of breaking the page. The claude.com recipe now uses it instead of the unwrap detour: it says what it means, and it is what a contributor should copy. Live-verified against the real page (10,509 chars, X-Source: recipe-content, all sections present); wikipedia still extracts via readability unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EF9FFG9xYHpyFexQQpe8CJ
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #44.
Root cause
The reported page's body is split across two separate containers, with an in-article call-to-action wedged between them. Readability scores a single top candidate and keeps that candidate plus its direct siblings — the second container is in a different branch, so the entire lead section (7 paragraphs plus a section heading) was dropped. The output still looked well-formed, which is what made this hard to spot.
Verified that vanilla Readability behaves identically on the raw HTML, so this is not caused by PullMD's
preprocess/cleanDomlayers.The Trafilatura auto-pick could not rescue it either: Trafilatura's output for this page carried the full text (9.8k vs 7.8k chars) but no markdown headings, and
pickBestrequirest.headings >= 1before preferring the longer candidate (lib/scoring.js). That guard is correct — it prevents nav-soup from winning — it just cannot help here.What this PR does
The reported page could have been patched with a two-line
unwraprecipe, and the first commit did exactly that. But the underlying gap is more general, so the PR closes it properly.1.
select.content— recipes can name the article bodyThe recipe engine was purely subtractive.
select.removeandpreprocesssay what to throw away; nothing said what the article is, so the final choice always stayed with Readability's scoring. For a split body, removal cannot help — the problem is not a surplus element, it is that half the article was never selected.Every match is joined in document order into one document and used as the body, skipping both Readability scoring and the Trafilatura auto-pick. Output carries
source: recipe-content.[".a", ".a p"]cannot emit prose twiceselect.removestill applies first, so the two composeSafety net. A stale
removeselector is harmless; a stalecontentselector would yield an empty article. So a selection under 200 chars falls back to the normal pipeline and records why inmetadata.extractorReason. A site redesign degrades the recipe to generic behavior instead of breaking the page.2. The shipped recipe uses it
{ "name": "claude-blog-split-body", "host": ["claude.com", "www.claude.com"], "path": "/blog/**", "select": { "content": [".u-rich-text-blog"] } }This replaces the
unwrapdetour. The unwrap worked, but it required knowing why Readability fails in order to understand it;contentsays what it means, and it is what a contributor should copy.Verification
End to end against the live page through a local server (
nocache=1):X-Sourcereadabilityrecipe-contentNo regression on unaffected sites: Wikipedia still extracts via
readabilityunchanged.GET /api/recipes/statusreports 7 loaded, 0 rejected.node --test: 995/995 pass (13 new).Tests
test/recipes-select-content.test.jscovers schema and merge-by-concatenation, document-order emission, nested-match dedupe, both fallback paths and theirextractorReason, invalid-selector isolation, andselect.removecomposing withselect.content.test/recipes-claude-blog.test.jsasserts the bug without the recipe, the fix with it, and that the recipe does not apply outside the matched path. The fixture is the real page's DOM skeleton with all prose replaced by same-length filler — the structure is what triggers the bug, the wording is not, so no third-party article text is committed.Docs
SITE-RECIPES.mdgets a full section onselect.content(semantics, when to reach for it versusremove, the safety net), plus updated schema and merge tables and an entry in the built-in examples. CHANGELOG under Added and Fixed.🤖 Generated with Claude Code
https://claude.ai/code/session_01EF9FFG9xYHpyFexQQpe8CJ