diff options
| author | Peter Stone <thepeterstone@gmail.com> | 2026-07-23 22:32:14 +0000 |
|---|---|---|
| committer | Peter Stone <thepeterstone@gmail.com> | 2026-07-23 22:32:14 +0000 |
| commit | ab392e2ab983a4c87e241b457c98244251b2206a (patch) | |
| tree | 706d82f65760ba130a705283bc90a7a431ac9a10 /app/src/main/java | |
| parent | ecdb45778e274126737aa87e27ed1b1c00cda01c (diff) | |
Strip <header> chrome before Readability parses; show full article title in reader content
- Confirmed live (Ars Technica): a section-kicker badge, the dek/subhead, and
an orphaned duplicate hero-image caption were all leaking into extracted
content because they live in a <header> Readability doesn't exclude by tag,
only deprioritizes in scoring. Strip <header> from the DOM ourselves before
handing off to Readability -- a pre-processing decision on our side, not a
change to Readability's own algorithm, per instruction not to touch it.
- Render the article's full title as a heading at the top of the reader
content itself (wrapReaderHtml), not just the top bar, which truncates to
one line. This also replaces whatever heading Readability happened to keep
in-body, now that <header> (and any <h1> inside it) is stripped pre-parse.
- Bump extraction timeout 15s -> 25s: confirmed live, several heavy
real-world pages were timing out before onPageFinished ever fired, well
short of a real parsing failure.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZ7ikw2ukGJFTHE3bJS7zL
Diffstat (limited to 'app/src/main/java')
4 files changed, 44 insertions, 7 deletions
diff --git a/app/src/main/java/org/terst/feedreader/readability/ReaderExtractor.kt b/app/src/main/java/org/terst/feedreader/readability/ReaderExtractor.kt index c41bbfb..f9adbb4 100644 --- a/app/src/main/java/org/terst/feedreader/readability/ReaderExtractor.kt +++ b/app/src/main/java/org/terst/feedreader/readability/ReaderExtractor.kt @@ -32,7 +32,12 @@ data class ExtractedArticle( * until the JS callback fires (or the timeout elapses). */ object ReaderExtractor { - private const val TIMEOUT_MS = 15_000L + // Confirmed live: a heavy, ad/tracker-laden real page (several Ars Technica articles) can + // legitimately take longer than 15s to finish loading everything before onPageFinished + // fires, causing extraction to time out and silently fall back to no content -- not a + // parsing failure, just not enough runway. 25s costs little (a spinner stays up slightly + // longer on a truly dead page) against a real reduction in spurious extraction failures. + private const val TIMEOUT_MS = 25_000L private val json = Json { ignoreUnknownKeys = true } suspend fun extract(context: Context, url: String): ExtractedArticle? = @@ -175,6 +180,25 @@ object ReaderExtractor { } } + // A news/blog article page's <header> conventionally holds everything + // BEFORE the real body: a section/category kicker, the dek/subhead, the + // byline, and the hero image + its caption. Readability doesn't exclude + // <header> by tag outright (only deprioritizes it in scoring), so on + // some articles enough of it scores highly enough to get pulled in + // alongside the real body -- confirmed live: a kicker icon, the dek + // paragraph, AND an orphaned duplicate hero-image caption (a mobile-only + // copy with no adjacent <img> at all) all leaking in above the real + // lede on an Ars Technica article. This is a pre-processing decision on + // our side, not a change to Readability's own algorithm: treat <header> + // as page chrome, never article content, and remove it before Readability + // ever sees the page. (The article's actual title is shown separately -- + // see ReaderWebView.wrapReaderHtml -- so losing the in-body <h1> here is + // fine, it was never meant to double as the body text anyway.) + var headers = document.querySelectorAll('header'); + for (var i = 0; i < headers.length; i++) { + headers[i].parentNode.removeChild(headers[i]); + } + // Readability's default hidden-element skip (treating display:none / // visibility:hidden nodes as boilerplate) is correct for the vast // majority of sites -- it's what keeps collapsed nav/footer/sidebar diff --git a/app/src/main/java/org/terst/feedreader/ui/LinkReaderScreen.kt b/app/src/main/java/org/terst/feedreader/ui/LinkReaderScreen.kt index 1a715b0..4c7251f 100644 --- a/app/src/main/java/org/terst/feedreader/ui/LinkReaderScreen.kt +++ b/app/src/main/java/org/terst/feedreader/ui/LinkReaderScreen.kt @@ -224,13 +224,13 @@ fun LinkReaderScreen(url: String, onBack: () -> Unit, onOpenLink: (String) -> Un modifier = Modifier.fillMaxSize(), factory = { ctx -> newReaderWebView(ctx, onOpenLink).apply { - loadDataWithBaseURL(url, wrapReaderHtml(currentExtractedContent), "text/html", "UTF-8", null) + loadDataWithBaseURL(url, wrapReaderHtml(currentExtractedContent, extractedTitle), "text/html", "UTF-8", null) loadedContent.value = currentExtractedContent } }, update = { webView -> if (loadedContent.value != currentExtractedContent) { - webView.loadDataWithBaseURL(url, wrapReaderHtml(currentExtractedContent), "text/html", "UTF-8", null) + webView.loadDataWithBaseURL(url, wrapReaderHtml(currentExtractedContent, extractedTitle), "text/html", "UTF-8", null) loadedContent.value = currentExtractedContent } }, diff --git a/app/src/main/java/org/terst/feedreader/ui/ReaderScreen.kt b/app/src/main/java/org/terst/feedreader/ui/ReaderScreen.kt index 6e1f939..5ebf2ba 100644 --- a/app/src/main/java/org/terst/feedreader/ui/ReaderScreen.kt +++ b/app/src/main/java/org/terst/feedreader/ui/ReaderScreen.kt @@ -469,13 +469,13 @@ private fun ReaderContent( modifier = Modifier.fillMaxSize(), factory = { ctx -> newReaderWebView(ctx, onOpenLink).apply { - loadDataWithBaseURL(baseUrl, wrapReaderHtml(content), "text/html", "UTF-8", null) + loadDataWithBaseURL(baseUrl, wrapReaderHtml(content, article?.title), "text/html", "UTF-8", null) loadedContent.value = content } }, update = { webView -> if (loadedContent.value != content) { - webView.loadDataWithBaseURL(baseUrl, wrapReaderHtml(content), "text/html", "UTF-8", null) + webView.loadDataWithBaseURL(baseUrl, wrapReaderHtml(content, article?.title), "text/html", "UTF-8", null) loadedContent.value = content } }, diff --git a/app/src/main/java/org/terst/feedreader/ui/ReaderWebView.kt b/app/src/main/java/org/terst/feedreader/ui/ReaderWebView.kt index 503f71e..f95dfed 100644 --- a/app/src/main/java/org/terst/feedreader/ui/ReaderWebView.kt +++ b/app/src/main/java/org/terst/feedreader/ui/ReaderWebView.kt @@ -28,14 +28,27 @@ fun newReaderWebView(context: Context, onOpenLink: (String) -> Unit): WebView = } } +private fun escapeHtml(text: String): String = text + .replace("&", "&") + .replace("<", "<") + .replace(">", ">") + .replace("\"", """) + .replace("'", "'") + // Hardcoded dark palette, not @media (prefers-color-scheme) -- the app itself always // forces dark colorScheme() regardless of system setting (see MainActivity), so reader // content should match rather than following a system light/dark the app itself ignores. -fun wrapReaderHtml(bodyContent: String): String = """ +// +// title is rendered here, as a real heading at the top of the content, rather than relying +// on whatever heading Readability happened to keep in bodyContent (increasingly nothing -- +// see ReaderExtractor's <header> stripping) or on the top app bar's title, which truncates +// to one line and was the only place a long title showed up before this. +fun wrapReaderHtml(bodyContent: String, title: String? = null): String = """ <html><head><meta name="viewport" content="width=device-width, initial-scale=1"> <style> body { font-family: sans-serif; font-size: 18px; line-height: 1.5; padding: 16px; background-color: #121212; color: #e1e1e1; } + h1.article-title { font-size: 1.5em; line-height: 1.3; margin: 0 0 0.6em 0; } a { color: #8ab4f8; } img { max-width: 100%; height: auto; } svg { max-width: 100%; } @@ -49,5 +62,5 @@ fun wrapReaderHtml(bodyContent: String): String = """ those alone -- just cap the ones that would otherwise size themselves. */ svg:not([width]):not([height]) { max-width: 32px; max-height: 32px; } </style></head> - <body>$bodyContent</body></html> + <body>${if (!title.isNullOrBlank()) "<h1 class=\"article-title\">${escapeHtml(title)}</h1>" else ""}$bodyContent</body></html> """.trimIndent() |
