Read-time: full-article "Full story · ~N min" badge (Option B)
Replaces the gist-based read-time with the SOURCE article's full read time — the
contrast that sells the gist ("calm 1-min version here; ~10 min for the deep dive").
- goodnews/readtime.py: word_count_from_html (strips script/style/nav/header/
footer/form/button/aside furniture before counting) + source_read_minutes
(~225 wpm, 200-word floor, None when extraction looks failed/too thin).
- articles.source_words + read_checked_at columns (count only, never the body;
fits the privacy posture). Idempotent migration.
- enrich.fetch_source_words + enrich_read_times: a bounded, retry-guarded cycle
step (mirrors the image enrichers) that counts words for recent accepted
articles. Only ever writes a real count; never overwrites good with zero. Wired
into the cycle after recent-image enrichment.
- queries: source_words flows through _ARTICLE_COLUMNS; api exposes
source_read_minutes on Article (null when unknown).
- home3: News card shows "Full story · ~N min", hidden entirely when null (no
misleading "1 min").
- Tests: furniture stripping, threshold/rounding, enrich idempotency + no
zero-overwrite, API null handling. 412 backend.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -26,10 +26,10 @@
|
||||
// truncation handled by CSS (-webkit-line-clamp:2) — breaks on whole words, fills 2 full lines
|
||||
let headline = $derived(news?.title ?? 'What went right this week: the good news that actually matters');
|
||||
|
||||
// Honest read-time from our own gist (~200 wpm, floor 1). We summarize, so this is
|
||||
// usually "1 min read" — a feature, not a bug: the good news in about a minute.
|
||||
const readMins = (t) => Math.max(1, Math.round((t || '').trim().split(/\s+/).filter(Boolean).length / 200));
|
||||
let readTime = $derived(`${readMins(news?.summary)} min read`);
|
||||
// The badge shows how long the FULL source article takes — the contrast that sells
|
||||
// the gist ("the calm 1-min version here; ~10 min if you want the deep dive"). Computed
|
||||
// server-side from the source word count; hidden entirely when we couldn't measure it.
|
||||
let fullRead = $derived(news?.source_read_minutes ? `Full story · ~${news.source_read_minutes} min` : '');
|
||||
|
||||
// small-joys shelf: 3 cells shown two at a time, rotated by the reader (no auto-motion)
|
||||
const JOY_ACCENTS = ['#4f7da8', '#b06a86', '#b06a45'];
|
||||
@@ -130,7 +130,7 @@
|
||||
<p class="summary">{news?.summary || "We read the week so you don't have to doomscroll it. Five quietly hopeful stories, summarised to the gist."}</p>
|
||||
</a>
|
||||
<div class="news-foot">
|
||||
<span class="meta">{readTime}</span>
|
||||
{#if fullRead}<span class="meta">{fullRead}</span>{/if}
|
||||
</div>
|
||||
<hr class="news-div" />
|
||||
<a class="news-more" href="/">Read more good news →</a>
|
||||
|
||||
Reference in New Issue
Block a user