Fix ArticleCard image fallback, build warning, and link safety

- Show the typographic fallback for missing images too (not only on load
  error), driven by component state instead of imperative class mutation —
  which also clears the unused-CSS-selector build warning.
- Only render external links for http(s) URLs, else href=#.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
jay
2026-05-30 22:39:22 +00:00
parent 5601022cf7
commit 14842127da
+18 -10
View File
@@ -1,18 +1,28 @@
<script>
let { article, onaction, hero = false } = $props();
// Drive the image/fallback from state, not imperative class mutation — so the
// typographic fallback shows for BOTH missing and broken images, and Svelte
// sees the styles as used (no unused-selector warning).
let imgOk = $state(!!article.image_url);
// Only ever link out to real http(s) URLs (the API is clean, but cheap defense).
const safeHref =
typeof article.url === 'string' && /^https?:\/\//.test(article.url) ? article.url : '#';
function act(kind, value) {
if (value) onaction?.(kind, value);
}
</script>
<article class:hero>
<a class="media" href={article.url} target="_blank" rel="noopener">
{#if article.image_url}
<a class="media" href={safeHref} target="_blank" rel="noopener">
{#if article.image_url && imgOk}
<img src={article.image_url} alt="" loading="lazy" referrerpolicy="no-referrer"
onerror={(e) => e.currentTarget.parentElement.classList.add('noimg')} />
onerror={() => (imgOk = false)} />
{:else}
<span class="fallback">{article.topic ?? 'good news'}</span>
{/if}
<span class="fallback">{article.topic ?? 'good news'}</span>
</a>
<div class="body">
@@ -22,7 +32,7 @@
<span class="src">{article.source}</span>
</div>
<h3><a href={article.url} target="_blank" rel="noopener">{article.title}</a></h3>
<h3><a href={safeHref} target="_blank" rel="noopener">{article.title}</a></h3>
{#if hero && article.description}
<p class="desc">{article.description}</p>
@@ -59,13 +69,11 @@
}
.media img { width: 100%; height: 100%; object-fit: cover; }
.fallback {
position: absolute; inset: 0; display: none;
position: absolute; inset: 0; display: flex;
align-items: center; justify-content: center;
font-family: var(--serif); font-style: italic; color: var(--sage-deep); opacity: 0.6;
text-transform: lowercase; letter-spacing: 0.02em;
font-family: var(--serif); font-style: italic; color: var(--sage-deep); opacity: 0.5;
text-transform: lowercase; letter-spacing: 0.02em; font-size: 1.1rem;
}
.media.noimg img { display: none; }
.media.noimg .fallback { display: flex; }
.body { padding: 16px 18px 14px; display: flex; flex-direction: column; gap: 8px; flex: 1; }
.tags { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; font-size: 0.74rem; }