From 54761f508344ebd69d0abc6bd89e8b7ec368b507 Mon Sep 17 00:00:00 2001 From: jay Date: Sun, 28 Jun 2026 16:21:42 -0400 Subject: [PATCH] news behavior split: /news leads with Latest, Highlights via ?view=highlights MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Base-aware so the frozen `/` is untouched (it's still the live, indexed site until cutover); the new behavior applies only to /news. At cutover `/` becomes the hub and only /news's behavior remains. - defaultView(base): /news bare → Latest (the live firehose); `/` bare → Highlights. - Brief is canonically /news?view=highlights, with ?view=today kept as an alias. - Latest is pure chronological on /news — stop passing `home` into it (geo scope belongs to Highlights). The Closer-to-Home card/dial is hidden on /news Latest; Highlights keeps the scope dial. `/`'s Latest keeps geo (frozen). - Back fixed: on /news it shows only for genuine drill-ins (tag/source/search), not the top-level lanes (Latest/Highlights/Following); `/` keeps its old rule. - goBack's app-safe fallback lands on the base's default view. feednav.js gains defaultView + def-aware parse/build; 36 frontend tests (9 new), build clean. /news stays noindex. Co-Authored-By: Claude Opus 4.8 --- frontend/src/lib/components/NewsFeed.svelte | 34 +++++++----- frontend/src/lib/feednav.js | 26 ++++++--- frontend/src/lib/feednav.test.js | 60 +++++++++++++-------- 3 files changed, 78 insertions(+), 42 deletions(-) diff --git a/frontend/src/lib/components/NewsFeed.svelte b/frontend/src/lib/components/NewsFeed.svelte index bc7d81d..a1c8318 100644 --- a/frontend/src/lib/components/NewsFeed.svelte +++ b/frontend/src/lib/components/NewsFeed.svelte @@ -18,7 +18,7 @@ import { track } from '$lib/analytics.js'; import { pwa, installApp, dismissPwa } from '$lib/pwa.svelte.js'; import { ritualState, markBriefSeen } from '$lib/ritual.js'; - import { feedBase, parseView, viewUrl } from '$lib/feednav.js'; + import { feedBase, defaultView, parseView, viewUrl } from '$lib/feednav.js'; // Which top bar to wear: 'legacy' = the feed's own Header (the interim `/` mount, kept // unchanged), 'hub' = the shared editorial HubBar (the /news mount, part of the new family). @@ -36,8 +36,18 @@ // so `/` behaves exactly as today and `/news` is self-coherent. At cutover, `/` becomes // the hub and only `/news` renders the feed. function base() { return feedBase($page.url.pathname); } - function urlForView(key) { return viewUrl(base(), key); } - let selected = $derived(parseView($page.url)); + function urlForView(key) { return viewUrl(base(), key, defaultView(base())); } + let selected = $derived(parseView($page.url, defaultView(feedBase($page.url.pathname)))); + // Latest is local-first only on the frozen `/`; on /news it's the pure chronological + // firehose — the geo scope dial belongs to Highlights, not Latest (Codex). + let latestGeo = $derived(chrome !== 'hub'); + // Back is for genuine drill-ins. On /news suppress it for the top-level lanes (Latest is + // the default there); on the frozen `/` keep the original "anything but Highlights" rule. + let showBack = $derived( + chrome === 'hub' + ? (selected.startsWith('tag:') || selected.startsWith('source:') || selected === 'search') + : selected !== 'today' + ); let searchQuery = $derived(($page.url.searchParams.get('q') || '').trim()); let searchOpen = $state(false); let searchText = $state(''); @@ -69,7 +79,7 @@ // Scope dial: the reader's "emotional radius" — nearby | region | country | world. // Persisted; default nearby. 'world' = the global brief/feed (no geo lead). let homeScope = $state('nearby'); - const homeActive = () => selected === 'latest' && !!homeValue; + const homeActive = () => latestGeo && selected === 'latest' && !!homeValue; let showSignIn = $state(false); let showSaved = $state(false); // Saved flyout let loading = $state(true); @@ -299,8 +309,8 @@ } if (key === 'latest') { const q = P.param(prefs.data); - // Closer to Home lives on the all-news browse lane (Latest). - const homeq = homeValue ? `&home=${encodeURIComponent(homeValue)}` : ''; + // Closer to Home lives on Highlights now; Latest stays pure chronological on /news. + const homeq = latestGeo && homeValue ? `&home=${encodeURIComponent(homeValue)}` : ''; return `/api/feed?sort=latest&limit=${PAGE}&offset=${offset}${homeq}${q ? '&' + q : ''}${exq}`; } if (key === 'following') { @@ -338,7 +348,7 @@ let appNavDepth = 0; function goBack() { if (appNavDepth > 0 && typeof history !== 'undefined') history.back(); - else goto(urlForView('today')); // landed here directly → app-safe Highlights + else goto(urlForView(defaultView(base()))); // landed directly → the base's default view } // Load the data for a view. Called by afterNavigate (URL-driven: in-app goto, @@ -359,7 +369,7 @@ feed = items; feedNextOffset = resp.next_offset ?? null; // Home lane pages by the API's world cursor; other lanes by simple length. - feedDone = (key === 'latest' && homeValue) ? feedNextOffset == null : items.length < PAGE; + feedDone = (key === 'latest' && latestGeo && homeValue) ? feedNextOffset == null : items.length < PAGE; markDisplayed(feed); if (key.startsWith('source:') && items[0]) { sourceNames = { ...sourceNames, [key.slice(7)]: items[0].source }; @@ -687,11 +697,7 @@ {isFollowing(followTarget.kind, followTarget.value) ? '✓ Following' : 'Follow ' + followTarget.noun} {/if} - - {#if selected !== 'today'} + {#if showBack}