Summary briefing layer: Today pre-summarized, /a is the canonical read

Make summaries the core reading experience (summary-first, source-forward):
- Cycle pre-warms summaries for Today's 7 (idempotent → only new ones hit the LLM).
- /api/brief items carry their cached summary; Today cards (hero + tiles) show it
  inline, so Today reads as a calm briefing.
- Card title/image now open the /a summary page (the canonical artifact), with a
  visible "Full story" link straight to the source on every card (the escape hatch).
- /a gains related-grouping chips + a Copy-link/share control.
- Tighten the summary prompt: original, factual, no quotations / no close paraphrase.
Long tail stays lazy+cached. No article bodies stored. 129 tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
jay
2026-06-04 19:48:32 +00:00
parent 3924d927aa
commit cfde4e22db
6 changed files with 80 additions and 14 deletions
+25 -6
View File
@@ -43,6 +43,12 @@
let safeHref = $derived(
typeof article.url === 'string' && /^https?:\/\//.test(article.url) ? article.url : '#'
);
// Default click → our summary page (the calm gist). Source is one tap further.
let summaryHref = $derived('/a/' + article.id);
function openedSource() {
onview?.(article);
track('open', article.id);
}
function act(kind, value) {
if (value) onaction?.(kind, value);
@@ -87,7 +93,7 @@
data-topic={article.topic ?? ''}
>
{#if showImage}
<a class="media" href={safeHref} target="_blank" rel="noopener" onclick={opened}>
<a class="media" href={summaryHref} onclick={opened}>
<img src={article.image_url} alt="" loading="lazy" referrerpolicy="no-referrer"
onerror={() => { failed = true; onimageerror?.(); }} />
</a>
@@ -108,9 +114,11 @@
</div>
<div class="src">{article.source}</div>
<h3><a href={safeHref} target="_blank" rel="noopener" onclick={opened}>{article.title}</a></h3>
<h3><a href={summaryHref} onclick={opened}>{article.title}</a></h3>
{#if hero && article.description}
{#if article.summary}
<p class="summary">{article.summary}</p>
{:else if hero && article.description}
<p class="desc">{article.description}</p>
{/if}
@@ -133,10 +141,11 @@
{isSaved ? 'Saved' : 'Save'}
</button>
{/if}
<a class="source" href={safeHref} target="_blank" rel="noopener" onclick={openedSource}>
{article.paywalled ? 'Full story 🔒' : 'Full story'}
</a>
{#if onreplace}
<button class="replace" onclick={() => onreplace(article)}>
{article.paywalled ? 'Find one I can read' : 'Replace'}
</button>
<button class="replace" onclick={() => onreplace(article)}>Replace</button>
{/if}
<span class="sharewrap">
<button class="share" onclick={() => { shareOpen = !shareOpen; if (shareOpen) warmSummary(); }} aria-expanded={shareOpen}>Share</button>
@@ -206,6 +215,11 @@
h3 { font-size: 1.18rem; }
h3 a:hover { color: var(--accent-deep); }
.desc { margin: 2px 0 0; color: #3b4754; }
.summary { margin: 2px 0 0; color: var(--ink); font-size: 0.92rem; }
.tile .summary {
display: -webkit-box; -webkit-box-orient: vertical; overflow: hidden; -webkit-line-clamp: 5;
}
.hero .summary, .herotype .summary { font-size: 1rem; }
.paywall {
margin: 0; font-size: 0.78rem; color: var(--gold);
display: inline-flex; align-items: center; gap: 5px;
@@ -235,6 +249,11 @@
color: var(--accent-deep); border-bottom-color: var(--accent-soft);
}
.actions .save.on { color: var(--accent); }
.actions .source {
color: var(--accent-deep); text-decoration: none; font-size: 0.76rem;
border-bottom: 1px dotted var(--accent-soft);
}
.actions .source:hover { border-bottom-color: var(--accent); }
.actions .share { color: var(--accent-deep); border-bottom-color: var(--accent-soft); }
.sharewrap { position: relative; display: inline-flex; }
.sharemenu {