images: cache + serve article images from our own origin (bounded, LRU-evicted)
Stop hotlinking news images from third-party CDNs (the source of the "blank until
you refresh a few times" graphic). New goodnews/newsimg.py caches a downscaled WebP
display copy (≤800px) beside the DB, like art_cache:
- GET/HEAD /api/img/{article_id} — resolves id→image_url (allowlisted to our corpus,
not an open proxy), fetch+cache on first miss, serve local after, immutable headers.
- cycle warms display copies for recent accepted-with-image articles (so the FIRST
view is already local) and prunes to a hard size cap (default 1 GB) by LRU eviction.
Frontend now points at /api/img/<id>: the hub lead, every ArticleCard (feed hero +
cards), and the /a/<id> share page's visible image. og:image/twitter:image stay the
source URL so social crawlers fetch the canonical image directly.
Storage is bounded by construction — over the cap, least-recently-used files are
evicted, so it can't grow without limit regardless of ingest rate. Tests cover
fetch/downscale, cache-hit (no refetch), bad-scheme/non-image rejection, fetch
failure, LRU prune, warm, and the endpoint allowlist.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -100,7 +100,8 @@
|
||||
>
|
||||
{#if showImage}
|
||||
<a class="media" href={summaryHref} onclick={opened}>
|
||||
<img src={article.image_url} alt="" loading="lazy" referrerpolicy="no-referrer"
|
||||
<!-- our cached/downscaled copy (/api/img/<id>) instead of hotlinking the source -->
|
||||
<img src={article.id ? `/api/img/${article.id}` : article.image_url} alt="" loading="lazy" referrerpolicy="no-referrer"
|
||||
onerror={() => { failed = true; onimageerror?.(); }} />
|
||||
</a>
|
||||
{:else if usePlaceholder}
|
||||
|
||||
@@ -112,7 +112,9 @@
|
||||
const q = P.param(prefs.data); // the reader's boundaries (excluded topics, ceilings)
|
||||
try {
|
||||
const it = (await getJSON(`/api/brief?limit=1${homeq}${q ? '&' + q : ''}`))?.items?.[0];
|
||||
if (it) news = { id: it.id, title: it.title, summary: it.summary || it.description || '', image: it.image_url || null, topic: it.topic || null, source_read_minutes: it.source_read_minutes };
|
||||
// Serve the photo from our own cached/downscaled copy (/api/img/<id>) rather than
|
||||
// hotlinking the source — keep image_url only as the "has a photo?" signal.
|
||||
if (it) news = { id: it.id, title: it.title, summary: it.summary || it.description || '', image: it.image_url ? `/api/img/${it.id}` : null, topic: it.topic || null, source_read_minutes: it.source_read_minutes };
|
||||
// News images are hotlinked from the source, so a single fetch can transiently
|
||||
// fail (slow CDN, rate limit, hiccup) and leave a blank plate until you refresh.
|
||||
// Probe with a couple of retries and reveal the photo only once it's truly loaded
|
||||
|
||||
Reference in New Issue
Block a user