diff --git a/app/index.html b/app/index.html index 7c1f2a7..fc60ea3 100644 --- a/app/index.html +++ b/app/index.html @@ -1276,7 +1276,7 @@ body { window.history.replaceState(null, '', url); maybeLoadCategoryFont(code).then(() => { renderNav(); - if (currentPage) navigateTo(currentPage); + if (currentPage) navigateTo(currentPage, { replace: true }); }); } @@ -3120,7 +3120,12 @@ function fmtDatetime(dtStr) { } // ─── Page loading ───────────────────────────────────────── - async function navigateTo(file) { + // opts.replace: replace the current history entry instead of pushing a new + // one. Used for the initial load and for back/forward (popstate/hashchange), + // where pushing would corrupt the history stack. User navigations push, so + // the browser Back button returns to the previous page. + async function navigateTo(file, opts) { + const replace = !!(opts && opts.replace); const contentEl = document.getElementById('pageContent'); // Guard the router: only fetch relative .md paths. This blocks loading @@ -3169,7 +3174,7 @@ function fmtDatetime(dtStr) { u.pathname = basePath; u.hash = '#' + file; } - window.history.replaceState(null, '', u); + window.history[replace ? 'replaceState' : 'pushState'](null, '', u); contentEl.innerHTML = '
'; @@ -3243,14 +3248,14 @@ function fmtDatetime(dtStr) { const page = getPageFromHash(); // Ignore in-page heading anchors (e.g. #installation) — only route real .md // pages. Without this, clicking a heading link wipes the page with a 404. - if (page && page !== currentPage && isSafePagePath(page)) navigateTo(page); + if (page && page !== currentPage && isSafePagePath(page)) navigateTo(page, { replace: true }); }); window.addEventListener('popstate', () => { const slug = window.location.pathname.replace(basePath, '').replace(/^\//, '').replace(/\/$/, ''); const pathPage = slug ? resolveSlugToFile(slug) : null; const page = pathPage || getPageFromHash(); - if (page && page !== currentPage && isSafePagePath(page)) navigateTo(page); + if (page && page !== currentPage && isSafePagePath(page)) navigateTo(page, { replace: true }); }); // ─── Scroll to top ─────────────────────────────────────── @@ -3352,7 +3357,7 @@ function fmtDatetime(dtStr) { } const hashPage = getPageFromHash(); - await navigateTo(routeFromPath || hashPage || defaultPage()); + await navigateTo(routeFromPath || hashPage || defaultPage(), { replace: true }); } catch (err) { document.getElementById('app').innerHTML = `

MD-CMS Error

diff --git a/app/service-worker.js b/app/service-worker.js index 791adf2..e86b91c 100644 --- a/app/service-worker.js +++ b/app/service-worker.js @@ -59,8 +59,18 @@ self.addEventListener('activate', event => { }); self.addEventListener('fetch', event => { - if (event.request.method !== 'GET') return; + const req = event.request; + if (req.method !== 'GET') return; + // App-shell routing: serve cached index.html for every navigation, including + // clean URLs like /section-id on reload. Without this the static host returns + // 404 for those paths before any JavaScript runs. Works offline too. + if (req.mode === 'navigate') { + event.respondWith( + caches.match('index.html').then(shell => shell || fetch(req)) + ); + return; + } event.respondWith( - caches.match(event.request).then(cached => cached || fetch(event.request)) + caches.match(req).then(cached => cached || fetch(req)) ); }); diff --git a/docs/unreleased.md b/docs/unreleased.md index bfc4ac5..f90e0a8 100644 --- a/docs/unreleased.md +++ b/docs/unreleased.md @@ -24,6 +24,15 @@ Changes merged into `development` that have not yet been released to `main`. - **Nested `mdcms` tags now hydrate.** Tags emitted inside a tab, accordion, or callout body (e.g. a post list inside a tab) are processed by re-sweeping until none remain, instead of rendering as empty divs. +- **Browser Back/Forward now navigate within the app.** User navigations use + `history.pushState` (the initial load, back/forward, and category re-renders + still replace), so Back returns to the previous page instead of leaving the + site. +- **Clean-URL reloads (Ctrl-R) work.** The generated service worker now serves + the cached `index.html` app shell for any navigation request, so reloading a + pretty URL such as `example.com/section-id` no longer 404s before the + client-side router runs. This also makes pretty-URL reloads work offline. + (Generated by `mdcms build`; `app/service-worker.js` regenerated.) - **`marked` is configured once** instead of re-registering the renderer on every page render. - Stored `md-cms-theme` value is validated against `light`/`dark` before use; diff --git a/mdcms.py b/mdcms.py index 886c791..3baf526 100644 --- a/mdcms.py +++ b/mdcms.py @@ -663,9 +663,19 @@ self.addEventListener('activate', event => {{ }}); self.addEventListener('fetch', event => {{ - if (event.request.method !== 'GET') return; + const req = event.request; + if (req.method !== 'GET') return; + // App-shell routing: serve cached index.html for every navigation, including + // clean URLs like /section-id on reload. Without this the static host returns + // 404 for those paths before any JavaScript runs. Works offline too. + if (req.mode === 'navigate') {{ + event.respondWith( + caches.match('index.html').then(shell => shell || fetch(req)) + ); + return; + }} event.respondWith( - caches.match(event.request).then(cached => cached || fetch(event.request)) + caches.match(req).then(cached => cached || fetch(req)) ); }}); """