Hero falls back to the next image when the lead's won't load
Some sources hotlink-protect their images (e.g. Guardian's i.guim.co.uk → 401), so a perfectly-enriched lead could still render an imageless hero. The browser is the only true judge of loadability, so on a hero image error, promote the next brief item that has an image into the hero slot; the failed lead becomes a text tile. Resets to the lead on each fresh brief. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
<script>
|
<script>
|
||||||
let { article, onaction, onreplace, ontag, hero = false } = $props();
|
let { article, onaction, onreplace, ontag, onimageerror, hero = false } = $props();
|
||||||
|
|
||||||
const humanize = (s) => (s || '').replace(/-/g, ' ');
|
const humanize = (s) => (s || '').replace(/-/g, ' ');
|
||||||
// Grouping tags are the doorways; cap at 3 so cards stay calm. Fall back to
|
// Grouping tags are the doorways; cap at 3 so cards stay calm. Fall back to
|
||||||
@@ -49,7 +49,7 @@
|
|||||||
{#if showImage}
|
{#if showImage}
|
||||||
<a class="media" href={safeHref} target="_blank" rel="noopener">
|
<a class="media" href={safeHref} target="_blank" rel="noopener">
|
||||||
<img src={article.image_url} alt="" loading="lazy" referrerpolicy="no-referrer"
|
<img src={article.image_url} alt="" loading="lazy" referrerpolicy="no-referrer"
|
||||||
onerror={() => (failed = true)} />
|
onerror={() => { failed = true; onimageerror?.(); }} />
|
||||||
</a>
|
</a>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
let families = $state([]);
|
let families = $state([]);
|
||||||
let selected = $state('today'); // 'today' | a mood key | a topic key | 'tag:<slug>'
|
let selected = $state('today'); // 'today' | a mood key | a topic key | 'tag:<slug>'
|
||||||
let brief = $state(null);
|
let brief = $state(null);
|
||||||
|
let heroIdx = $state(0); // which brief item fills the hero (advances if its image won't load)
|
||||||
let feed = $state([]);
|
let feed = $state([]);
|
||||||
let userPrefs = $state(P.blank());
|
let userPrefs = $state(P.blank());
|
||||||
let showBoundaries = $state(false);
|
let showBoundaries = $state(false);
|
||||||
@@ -87,6 +88,18 @@
|
|||||||
);
|
);
|
||||||
let activeTab = $derived(showYou ? 'you' : selected === 'today' ? 'today' : 'browse');
|
let activeTab = $derived(showYou ? 'you' : selected === 'today' ? 'today' : 'browse');
|
||||||
|
|
||||||
|
// The hero is the only image slot. Some sources hotlink-protect their images
|
||||||
|
// (e.g. Guardian → 401), so if the lead's image won't load, promote the next
|
||||||
|
// brief item that has one. The failed lead just becomes a text tile.
|
||||||
|
let heroArticle = $derived(brief?.items?.[heroIdx] ?? null);
|
||||||
|
let restArticles = $derived((brief?.items ?? []).filter((_, i) => i !== heroIdx));
|
||||||
|
function heroImageFailed() {
|
||||||
|
const items = brief?.items ?? [];
|
||||||
|
for (let i = heroIdx + 1; i < items.length; i++) {
|
||||||
|
if (items[i]?.image_url) { heroIdx = i; return; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// The filter for the current view: a mood's preset, a topic include, or none.
|
// The filter for the current view: a mood's preset, a topic include, or none.
|
||||||
function viewFilter(key = selected) {
|
function viewFilter(key = selected) {
|
||||||
if (key === 'today') return {};
|
if (key === 'today') return {};
|
||||||
@@ -111,6 +124,7 @@
|
|||||||
brief = fetched;
|
brief = fetched;
|
||||||
P.saveJSON(BRIEF_VIEW_KEY, { generated_at: fetched.generated_at, items: fetched.items });
|
P.saveJSON(BRIEF_VIEW_KEY, { generated_at: fetched.generated_at, items: fetched.items });
|
||||||
}
|
}
|
||||||
|
heroIdx = 0; // fresh brief — start the hero at the lead again
|
||||||
remember(brief.items);
|
remember(brief.items);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,7 +172,7 @@
|
|||||||
async function replaceArticle(article) {
|
async function replaceArticle(article) {
|
||||||
const list = selected === 'today' ? brief?.items : feed;
|
const list = selected === 'today' ? brief?.items : feed;
|
||||||
if (!list) return;
|
if (!list) return;
|
||||||
const isHero = selected === 'today' && list[0]?.id === article.id;
|
const isHero = selected === 'today' && heroArticle?.id === article.id;
|
||||||
const exclude = Array.from(seenIds).join(',');
|
const exclude = Array.from(seenIds).join(',');
|
||||||
const q = mergedParam();
|
const q = mergedParam();
|
||||||
const url = `/api/replacement?exclude=${exclude}&avoid_paywall=true${isHero ? '&gentle=true' : ''}${q ? '&' + q : ''}`;
|
const url = `/api/replacement?exclude=${exclude}&avoid_paywall=true${isHero ? '&gentle=true' : ''}${q ? '&' + q : ''}`;
|
||||||
@@ -281,10 +295,10 @@
|
|||||||
{#if selected === 'today'}
|
{#if selected === 'today'}
|
||||||
{#if brief?.items?.length}
|
{#if brief?.items?.length}
|
||||||
<section class="rise">
|
<section class="rise">
|
||||||
<ArticleCard article={brief.items[0]} hero onaction={applyAction} onreplace={replaceArticle} ontag={(t) => select('tag:' + t)} />
|
<ArticleCard article={heroArticle} hero onaction={applyAction} onreplace={replaceArticle} ontag={(t) => select('tag:' + t)} onimageerror={heroImageFailed} />
|
||||||
{#if brief.items.length > 1}
|
{#if restArticles.length}
|
||||||
<div class="grid rest">
|
<div class="grid rest">
|
||||||
{#each brief.items.slice(1) as a (a.id)}
|
{#each restArticles as a (a.id)}
|
||||||
<ArticleCard article={a} onaction={applyAction} onreplace={replaceArticle} ontag={(t) => select('tag:' + t)} />
|
<ArticleCard article={a} onaction={applyAction} onreplace={replaceArticle} ontag={(t) => select('tag:' + t)} />
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user