Bounded hero-image enrichment (og:image for brief items only)

The grid stays typographic; the hero is the one intentional visual slot. At
brief-build time we fetch a hero-quality image for the daily five that lack one:
- enrich.py reads ONLY a page's <head> og:image/twitter:image and stores just
  the URL (never the body).
- SSRF-guarded: http(s) only, 6s timeout, 300KB cap, <=3 manual redirects each
  re-validated, and hosts rejected if any resolved address is private, loopback,
  link-local, multicast, reserved, or unspecified.
- image_checked_at column caches success AND failure, so an article is never
  retried forever.
- Wired into build-brief and cycle (brief items only, only if image missing and
  unchecked). Everything else stays metadata-only.
- Verified live: today's five all carry images (feed + enriched).

Tests: og:image parser, head-only scope, IP guard across internal ranges, and
enrich success + failure-caching (85 total).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
jay
2026-05-31 12:37:41 +00:00
parent 3858380ffe
commit 9e8eddf46d
5 changed files with 230 additions and 1 deletions
+8
View File
@@ -129,6 +129,14 @@ If `frontend/build` is absent, the server falls back to the legacy single-page
harness in `goodnews/static/`. The Docker image builds the frontend automatically
(multi-stage), so deployment is just `docker build`.
The secondary grid is intentionally typographic (no thumbnails). The **hero** is
the one image slot: at brief-build time, `enrich.py` fetches a hero-quality image
for the daily five that lack one — reading **only** a page's `<head>` og:image /
twitter:image and storing just the URL (never the body). It's SSRF-guarded
(http(s) only, short timeout, byte cap, capped redirects, and rejects hosts that
resolve to private/loopback/link-local/multicast addresses), and failures are
cached so an article is never retried. Everywhere else stays metadata-only.
## Calm Filters
Personal, device-local controls so a reader can stay informed without subjects