From eb91a2f8565250ad14f22b76d1d34bb4829965e7 Mon Sep 17 00:00:00 2001 From: jay Date: Mon, 8 Jun 2026 09:11:24 -0400 Subject: [PATCH] Nav hardening: app-safe deep-link Back + stale-load guard on Today MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per Codex audit follow-ups: * Track in-app navigation depth (forward goto/link increments, popstate unwinds, clamped at 0) and base the in-page Back on it instead of history.length. A direct deep link (email/social/article) now sends the in-page Back to Highlights rather than out of the site. * Apply the same stale-load guard to the Today/Highlights path that feed views have, and only scroll-to-top when the load is still current — avoids stale error/scroll state during quick navigation. Co-Authored-By: Claude Opus 4.8 (1M context) --- frontend/src/routes/+page.svelte | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/frontend/src/routes/+page.svelte b/frontend/src/routes/+page.svelte index fd25d75..b11cd05 100644 --- a/frontend/src/routes/+page.svelte +++ b/frontend/src/routes/+page.svelte @@ -236,9 +236,13 @@ if (source) sourceNames = { ...sourceNames, [source.id]: source.name }; navigate(key); } + // How many forward in-app navigations deep we are from the landing entry, so + // the in-page Back stays INSIDE the app even when someone deep-links in from + // email/social/an article (history.length can't tell us that). + let appNavDepth = 0; function goBack() { - if (typeof history !== 'undefined' && history.length > 1) history.back(); - else goto(urlForView('today')); + if (appNavDepth > 0 && typeof history !== 'undefined') history.back(); + else goto(urlForView('today')); // landed here directly → app-safe Highlights } // Load the data for a view. Called by afterNavigate (URL-driven: in-app goto, @@ -251,6 +255,7 @@ try { if (key === 'today') { await loadToday(fresh); + if (seq !== loadSeq) return; // a newer navigation superseded this one } else { const items = (await getJSON(feedUrl(key, 0))).items; if (seq !== loadSeq) return; @@ -264,13 +269,17 @@ } catch (e) { if (seq === loadSeq) error = 'Something went quiet — could not reach the feed.'; } - if (typeof window !== 'undefined') window.scrollTo({ top: 0, behavior: 'smooth' }); + if (seq === loadSeq && typeof window !== 'undefined') window.scrollTo({ top: 0, behavior: 'smooth' }); } // Every URL change (in-app goto, browser back/forward, link) reloads the view. // The initial 'enter' is handled by onMount, after its data deps are fetched. afterNavigate((nav) => { if (nav.type === 'enter') return; + // Forward in-app nav deepens the stack; back/forward (popstate) unwinds it. + // Clamped at 0, so an out-of-app landing keeps Back app-safe (→ Highlights). + if (nav.type === 'goto' || nav.type === 'link') appNavDepth += 1; + else if (nav.type === 'popstate') appNavDepth = Math.max(0, appNavDepth - 1); loadView(selected); });