Enable Back button and fix clean-URL reloads

Renderer (app/index.html):
- navigateTo now pushes a history entry for user navigations (pushState),
  while the initial load, back/forward (popstate/hashchange), and category
  re-renders still replaceState. The browser Back button now returns to the
  previous page instead of leaving the site.

Service worker (mdcms.py generator + app/service-worker.js):
- Serve the cached index.html app shell for navigation requests. Reloading a
  clean URL like /section-id previously 404'd on the static host before any
  JavaScript ran; the shell fallback lets the client-side router resolve the
  path. Also makes pretty-URL reloads work offline.

https://claude.ai/code/session_018KXUwmSNMGF2UBywTChCcS
This commit is contained in:
Claude 2026-06-12 07:16:12 +00:00
parent df0f179004
commit a5127727f0
No known key found for this signature in database
4 changed files with 44 additions and 10 deletions

View file

@ -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 = '<div class="loading-spinner"></div>';
@ -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 = `<div style="max-width:600px;margin:4rem auto;padding:2rem;text-align:center;font-family:system-ui;">
<h1 style="color:#EF4444;font-size:1.5rem;">MD-CMS Error</h1>

View file

@ -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))
);
});

View file

@ -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;

View file

@ -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))
);
}});
"""