From 0b0fb8d199a74dcc2e115d74c7578169c0ea143b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Kr=C3=BCger?= Date: Sat, 29 Aug 2026 23:34:51 +0200 Subject: [PATCH] fix(lightbox): slideshow navigation writes browser history MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit goToSlide() was replaceState()-ing, so paging through the viewer left a single history entry — browser Back skipped every plate you'd looked at. It now pushState()s on each user-driven slide (guarded to fire only when the index actually changes), popstate slides the viewer back/forward without stacking entries, and the first entry on a directly-loaded plate page is tagged with its slug so Back to it is handled in-place. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01DZPmxGywFnAhmYJB1eh9fm --- static/js/app.js | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/static/js/app.js b/static/js/app.js index ee08ffb..29444e5 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -289,9 +289,14 @@ preloadNeighbors(lbIdx); } - function goToSlide(idx, smooth = true) { + // smooth — animate the track (false = jump, used when first opening) + // record — push a history entry + sync /meta (default follows `smooth`; + // popstate passes false so back/forward doesn't stack entries) + function goToSlide(idx, smooth = true, record = smooth) { if (!lbTrack) return; - lbIdx = Math.max(0, Math.min(idx, lbList.length - 1)); + const next = Math.max(0, Math.min(idx, lbList.length - 1)); + const changed = next !== lbIdx; + lbIdx = next; lbTrack.style.transition = smooth ? '' : 'none'; lbTrack.style.transform = `translateX(-${lbIdx * 100}%)`; if (!smooth) lbTrack.getBoundingClientRect(); // force reflow @@ -302,17 +307,17 @@ lbBuildMeta(p); syncThumbs(); preloadNeighbors(lbIdx); - if (p && smooth) { + if (p) { const postTitle = p.title + ' — Roux'; const postUrl = new URL(p.url, location.origin).href; document.title = postTitle; - history.replaceState({ slug: p.slug }, '', p.url); setMeta('name', 'description', p.description || null); setMeta('property', 'og:title', postTitle); setMeta('property', 'og:description', p.description || null); setMeta('property', 'og:url', postUrl); const canon = document.querySelector('link[rel="canonical"]'); if (canon) canon.href = postUrl; + if (record && changed) history.pushState({ slug: p.slug }, '', p.url); } } @@ -440,15 +445,19 @@ // Handle browser back/forward. window.addEventListener('popstate', e => { const slug = e.state && e.state.slug; - // Entry points at a plate we already know about → show it in the lightbox now, - // no page fetch needed (covers sliding + card-opened plates). if (slug && POSTS.some(p => p.slug === slug)) { + // Already in the viewer with this plate in the list → just slide to it. + if (lb && lb.dataset.open === 'true') { + const i = lbList.findIndex(p => p.slug === slug); + if (i !== -1) { goToSlide(i, true, false); return; } + } + // Otherwise (re)open the viewer at this plate — no page fetch needed. const opened = POSTS.find(p => p.slug === slug); const scoped = POSTS.filter(p => p.issue === opened.issue); lbOpen(slug, scoped.length ? scoped : POSTS); return; } - // Otherwise re-render #content for the URL we landed on (also closes the lightbox). + // Not a plate → re-render #content for the URL we landed on (also closes the viewer). navigate(location.href, { push: false }); }); @@ -562,6 +571,8 @@ const opened = POSTS.find(p => p.slug === window.__ROUX_OPEN_SLUG); const issueId = opened ? opened.issue : null; const scoped = issueId ? POSTS.filter(p => p.issue === issueId) : POSTS; + // Tag this first history entry so back-navigation to it is handled by the viewer. + if (opened) history.replaceState({ slug: opened.slug }, '', location.href); lbOpen(window.__ROUX_OPEN_SLUG, scoped.length ? scoped : POSTS); }