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