home: reveal the news photo only once it actually loads (retry + graceful fallback)
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 <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,7 @@
|
|||||||
let news = $state(null); // {id, title, summary, image, topic, ...}
|
let news = $state(null); // {id, title, summary, image, topic, ...}
|
||||||
let art = $state(null); // {title, artist, year, image}
|
let art = $state(null); // {title, artist, year, image}
|
||||||
let newsFit = $state('cover'); // 'cover' = full-bleed photo; 'contain' = framed-plate figure
|
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
|
// 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.
|
// well: the topic word on a soft topic-tinted field, color-coded across the feed.
|
||||||
@@ -112,14 +113,25 @@
|
|||||||
try {
|
try {
|
||||||
const it = (await getJSON(`/api/brief?limit=1${homeq}${q ? '&' + q : ''}`))?.items?.[0];
|
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 };
|
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) {
|
if (news?.image) {
|
||||||
const probe = new Image();
|
const url = news.image;
|
||||||
probe.onload = () => {
|
let tries = 0;
|
||||||
const a = probe.naturalWidth / probe.naturalHeight;
|
const load = () => {
|
||||||
newsFit = a >= 0.85 && a <= 1.9 ? 'cover' : 'contain';
|
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 */ }
|
} catch { /* fall back to design copy */ }
|
||||||
|
|
||||||
@@ -178,7 +190,7 @@
|
|||||||
<!-- Good News (tall) — a card with TWO links, so it's a div, not a single anchor -->
|
<!-- Good News (tall) — a card with TWO links, so it's a div, not a single anchor -->
|
||||||
<div class="card news">
|
<div class="card news">
|
||||||
<a class="news-photo-a" href={news?.id ? `/a/${news.id}` : '/'} aria-label="Read this article">
|
<a class="news-photo-a" href={news?.id ? `/a/${news.id}` : '/'} aria-label="Read this article">
|
||||||
{#if news?.image}
|
{#if news?.image && newsImgOk}
|
||||||
<div class="news-photo {newsFit}">
|
<div class="news-photo {newsFit}">
|
||||||
<div class="news-plate" style={`background-image:url(${news.image})`}></div>
|
<div class="news-plate" style={`background-image:url(${news.image})`}></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -383,7 +395,9 @@
|
|||||||
.headline-a:hover h2 { color: var(--teal); }
|
.headline-a:hover h2 { color: var(--teal); }
|
||||||
/* Photos fill edge-to-edge (cover, no box). Only figures/diagrams (detected by their
|
/* Photos fill edge-to-edge (cover, no box). Only figures/diagrams (detected by their
|
||||||
wide/tall shape) get the soft tinted matte + white framed plate, so labels stay whole. */
|
wide/tall shape) get the soft tinted matte + white framed plate, so labels stay whole. */
|
||||||
.news-photo { aspect-ratio: 5/4; }
|
.news-photo { aspect-ratio: 5/4; animation: news-photo-in 0.45s ease both; }
|
||||||
|
@keyframes news-photo-in { from { opacity: 0; } to { opacity: 1; } }
|
||||||
|
@media (prefers-reduced-motion: reduce) { .news-photo { animation: none; } }
|
||||||
.news-plate { background-position: center; background-repeat: no-repeat; }
|
.news-plate { background-position: center; background-repeat: no-repeat; }
|
||||||
.news-photo.cover .news-plate { width: 100%; height: 100%; background-size: cover; }
|
.news-photo.cover .news-plate { width: 100%; height: 100%; background-size: cover; }
|
||||||
.news-photo.contain {
|
.news-photo.contain {
|
||||||
|
|||||||
Reference in New Issue
Block a user