Source feeds: click a source to see its publication feed

Click a source name on any card → a feed of just that source's articles,
newest-first, still accepted / non-duplicate / boundary-filtered (the calm
promise isn't bypassed). A natural way to follow a publication's feel.

* queries.feed + /api/feed: source_id filter; Article output gains source_id.
* Frontend: source label is a button → transient 'source:<id>' view (like
  'tag:<slug>'), rendered in the feed grid with Load more, header = source name.
* Ad-hoc, not a pinned lane. Foundation for a future source page (metadata) +
  Follow; shareable /source/<slug> route and source_view analytics come then.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
jay
2026-06-08 08:30:33 -04:00
parent 50dc2167cd
commit 38889f76e5
5 changed files with 55 additions and 8 deletions
+19 -4
View File
@@ -20,7 +20,8 @@
let families = $state([]);
let lanePool = $state(null); // /api/lanes: { pinned, default, groups }
let showLanes = $state(false);
let selected = $state('today'); // 'today' | a mood key | a topic key | 'tag:<slug>'
let selected = $state('today'); // 'today' | mood | topic | 'tag:<slug>' | 'source:<id>'
let currentSource = $state(null); // {id, name} for a 'source:<id>' view's header
let brief = $state(null);
let heroIdx = $state(0);
let feed = $state([]);
@@ -98,12 +99,14 @@
);
let viewLabel = $derived(
selected === 'today' ? 'Highlights from Today'
: selected.startsWith('source:') ? (currentSource?.name ?? 'Source')
: selected === 'latest' ? 'Latest'
: currentTag ? humanize(currentTag)
: (currentMood?.label ?? cap(currentTopic?.key) ?? '')
);
let viewSubtitle = $derived(
selected === 'today' ? localDateLabel(brief)
: selected.startsWith('source:') ? 'Latest from this source'
: selected === 'latest' ? 'Freshest calm reads — newest first'
: currentTag ? (tagFamily?.description ?? '')
: (currentMood?.description ?? currentTopic?.description ?? '')
@@ -195,10 +198,22 @@
const q = P.param(prefs.data);
return `/api/feed?limit=${PAGE}&offset=${offset}&tag=${encodeURIComponent(key.slice(4))}${q ? '&' + q : ''}${exq}`;
}
if (key.startsWith('source:')) {
// A publication feed: this source's articles, newest first, still
// accepted/non-duplicate/boundary-filtered.
const q = P.param(prefs.data);
return `/api/feed?source_id=${encodeURIComponent(key.slice(7))}&sort=latest&limit=${PAGE}&offset=${offset}${q ? '&' + q : ''}${exq}`;
}
const q = P.param(P.merge(prefs.data, viewFilter(key)));
return `/api/feed?limit=${PAGE}&offset=${offset}${q ? '&' + q : ''}${exq}`;
}
// Clicking a source name opens its publication feed; stash the name for the header.
function selectSource(id, name) {
currentSource = { id, name };
select('source:' + id);
}
async function select(key, fresh = false) {
selected = key;
error = '';
@@ -346,11 +361,11 @@
{#if selected === 'today'}
{#if brief?.items?.length}
<section class="rise">
<ArticleCard article={heroArticle} hero onaction={applyAction} onreplace={replaceArticle} ontag={(t) => select('tag:' + t)} onview={record} onimageerror={heroImageFailed} />
<ArticleCard article={heroArticle} hero onaction={applyAction} onreplace={replaceArticle} ontag={(t) => select('tag:' + t)} onsource={selectSource} onview={record} onimageerror={heroImageFailed} />
{#if restArticles.length}
<div class="grid rest">
{#each restArticles as a (a.id)}
<ArticleCard article={a} thumb onaction={applyAction} onreplace={replaceArticle} ontag={(t) => select('tag:' + t)} onview={record} />
<ArticleCard article={a} thumb onaction={applyAction} onreplace={replaceArticle} ontag={(t) => select('tag:' + t)} onsource={selectSource} onview={record} />
{/each}
</div>
{/if}
@@ -362,7 +377,7 @@
{:else if feed.length}
<div class="grid rise">
{#each feed as a (a.id)}
<ArticleCard article={a} thumb onaction={applyAction} onreplace={replaceArticle} ontag={(t) => select('tag:' + t)} onview={record} />
<ArticleCard article={a} thumb onaction={applyAction} onreplace={replaceArticle} ontag={(t) => select('tag:' + t)} onsource={selectSource} onview={record} />
{/each}
</div>
{#if !feedDone}