From cb06d550bdb28d946cfa08a34e335d66e53abf06 Mon Sep 17 00:00:00 2001 From: jay Date: Mon, 29 Jun 2026 19:58:26 -0400 Subject: [PATCH] home: reveal the news photo only once it actually loads (retry + graceful fallback) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The hub painted the lead news image as a CSS background straight from the source's hotlinked URL — one transient failure (slow/rate-limited third-party CDN) left a blank plate until you refreshed and the browser served it from cache. Now the probe that already runs for cover-vs-figure detection gates the photo: load with up to two retries (0.5s/1s backoff), reveal the plate only once it's truly loaded (and cached), and otherwise keep the typographic topic cover. Soft fade-in on arrival; reduced-motion honored. No more blank-until-refresh. Co-Authored-By: Claude Opus 4.8 --- frontend/src/routes/+page.svelte | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/frontend/src/routes/+page.svelte b/frontend/src/routes/+page.svelte index 32fba52..1968f15 100644 --- a/frontend/src/routes/+page.svelte +++ b/frontend/src/routes/+page.svelte @@ -12,6 +12,7 @@ let news = $state(null); // {id, title, summary, image, topic, ...} let art = $state(null); // {title, artist, year, image} let newsFit = $state('cover'); // 'cover' = full-bleed photo; 'contain' = framed-plate figure + let newsImgOk = $state(false); // reveal the photo only once it's truly loaded (else the typo cover stays) // Pictureless articles (~half the feed) get a typographic category cover instead of a blank // well: the topic word on a soft topic-tinted field, color-coded across the feed. @@ -112,14 +113,25 @@ 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 }; - // Photos display full (cover); only wide/tall figures (diagrams) get the framed plate. + // 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 + // (and thus cached) — otherwise the typographic topic cover stays. The probe also + // tells us cover (photo) vs. framed plate (wide/tall figure) from real dimensions. if (news?.image) { - const probe = new Image(); - probe.onload = () => { - const a = probe.naturalWidth / probe.naturalHeight; - newsFit = a >= 0.85 && a <= 1.9 ? 'cover' : 'contain'; + const url = news.image; + let tries = 0; + const load = () => { + const probe = new Image(); + probe.onload = () => { + const a = probe.naturalWidth / probe.naturalHeight; + newsFit = a >= 0.85 && a <= 1.9 ? 'cover' : 'contain'; + newsImgOk = true; + }; + probe.onerror = () => { if (++tries <= 2) setTimeout(load, 500 * tries); }; + probe.src = url; }; - probe.src = news.image; + load(); } } catch { /* fall back to design copy */ } @@ -178,7 +190,7 @@