diff --git a/app/index.html b/app/index.html index c530c36..f9c18f3 100644 --- a/app/index.html +++ b/app/index.html @@ -1135,11 +1135,19 @@ body { if (b) b.remove(); } + function _isMdResponse(r) { + // Reject HTML responses — servers with SPA routing (e.g. Cloudflare Pages with + // "/* /index.html 200") return index.html with 200 for missing files, which would + // be mistaken for a found markdown file. + const ct = r.headers.get('content-type') || ''; + return !ct.startsWith('text/html'); + } + async function fetchPageFile(conceptualFile) { // conceptualFile like "pages/foo.md". Returns { ok, text, resolvedFile } or { ok: false }. if (!categoriesUse) { const r = await fetch(conceptualFile); - if (r.ok) return { ok: true, text: await r.text(), resolvedFile: conceptualFile }; + if (r.ok && _isMdResponse(r)) return { ok: true, text: await r.text(), resolvedFile: conceptualFile }; return { ok: false }; } const base = conceptualFile.replace(/\.md$/, ''); @@ -1169,7 +1177,7 @@ body { if (seen.has(url)) continue; seen.add(url); const r = await fetch(url); - if (r.ok) return { ok: true, text: await r.text(), resolvedFile: url }; + if (r.ok && _isMdResponse(r)) return { ok: true, text: await r.text(), resolvedFile: url }; } return { ok: false }; } diff --git a/docs/knownbugs.md b/docs/knownbugs.md index 5999aa0..e341d28 100644 --- a/docs/knownbugs.md +++ b/docs/knownbugs.md @@ -6,6 +6,26 @@ Bugs that have been identified but not yet fixed. Fixed bugs are moved to the re ## Fixed in development (not yet released) +### Category-variant pages fail to load on servers with SPA routing + +**Symptom:** On Cloudflare Pages (and any other server configured to serve `index.html` with HTTP 200 for missing paths), clicking a nav item whose page only exists as a category-variant file (e.g. `page.current.md`, no plain `page.md`) showed garbled content — the raw HTML of `index.html` rendered as markdown, with the site's `` text visible in the content area. + +**Root cause:** `fetchPageFile` tried the base filename (`pages/page.md`) first. Servers with SPA routing return this with HTTP 200 (serving `index.html`), so `r.ok` was true and the function returned without trying the actual variant file (`pages/page.current.md`). + +**Fix:** `fetchPageFile` now checks the `Content-Type` response header and skips any response with `text/html`, continuing to the next candidate URL. + +--- + +### Stale service worker not removed when `pwa: no` + +**Symptom:** After changing a site from `pwa: yes` to `pwa: no` and rebuilding, the old service worker remained active in browsers that had previously visited the site. Cached responses from the old build continued to be served. + +**Root cause:** `mdcms build` stopped generating PWA files when `pwa: no`, but `index.html` unconditionally registers `service-worker.js` on every page load. With no new SW to replace it, the old worker stayed installed indefinitely. + +**Fix:** `mdcms build` now writes a self-unregistering stub `service-worker.js` when `pwa: no`. On the visitor's next visit, the browser installs the stub which immediately calls `self.registration.unregister()`, evicting the stale worker. `manifest.json` is also deleted if present. + +--- + ### `config.yml` YAML parse errors were silently swallowed **Symptom:** A malformed `config.yml` (e.g. a stray tab character, which YAML forbids as a token starter) caused `read_config` to catch the `YAMLError` and return an empty dict. The build would proceed with no config — categories disabled, no default category code — producing a `nav.yml` that omitted `variants` fields and listed category variant files (e.g. `page.current.md`) as plain pages. Pages with category variants would not appear in the sidebar. diff --git a/docs/unreleased.md b/docs/unreleased.md index 7cc5389..36c6127 100644 --- a/docs/unreleased.md +++ b/docs/unreleased.md @@ -43,6 +43,22 @@ A rebuild is required for existing sites to pick up the change. --- +## Fix: category-variant pages fail to load on servers with SPA routing (e.g. Cloudflare Pages) + +When a site uses category-suffixed page files (e.g. `page.current.md`) and is hosted on a server configured with SPA fallback routing (serving `index.html` with HTTP 200 for any unknown path), the renderer's `fetchPageFile` mistook the HTML fallback for a found markdown file. It returned `index.html` content instead of falling through to try the `.current.md` variant. The page rendered the raw HTML of `index.html` as markdown, showing the `<title>` text (`sitename`) in the content area. + +`fetchPageFile` now checks the `Content-Type` response header and rejects any response with `text/html`, continuing to the next candidate URL instead. + +--- + +## Fix: stale service worker not removed when `pwa: no` + +`index.html` unconditionally registers `service-worker.js` on every page load. When a site switched from `pwa: yes` to `pwa: no`, `mdcms build` stopped generating a new service worker, but the old one remained active in browsers that had visited the site before. The stale worker continued to serve cached responses from the old build. + +`mdcms build` now writes a self-unregistering `service-worker.js` when `pwa: no`. On the visitor's next page load, the browser installs this stub worker, which immediately unregisters itself and evicts any previously cached content. `manifest.json` is also removed if present. + +--- + ## Fix: `config.yml` YAML parse errors now abort the build with a clear message A malformed `config.yml` (e.g. a stray tab character, which YAML forbids as a token starter) previously caused `read_config` to silently return an empty dict. The build would proceed with no config — categories disabled, no default category code — producing a broken `nav.yml` with wrong filenames and missing `variants` fields, so category-variant pages would not appear in the sidebar. diff --git a/mdcms.py b/mdcms.py index 9196751..ce4316e 100644 --- a/mdcms.py +++ b/mdcms.py @@ -528,6 +528,8 @@ def run_build(site_path: Path): pwa_enabled = str(cfg.get("pwa", "no")).lower() in ("yes", "true") if pwa_enabled: generate_pwa(site_path, cfg) + else: + cleanup_pwa(site_path) asset_warnings = validate_assets(site_path, cfg) for w in asset_warnings: @@ -543,6 +545,29 @@ def run_build(site_path: Path): # ─── PWA generation ─────────────────────────────────────────── +def cleanup_pwa(site_path: Path): + """When pwa: no, write a self-unregistering service worker and remove manifest.json. + + Browsers keep the previously installed service worker active until a new one is + installed. Writing a stub that immediately unregisters itself ensures any stale + caching worker is evicted on the next visit after a pwa: yes → pwa: no change. + """ + sw = site_path / "service-worker.js" + sw.write_text( + "// mdcms: PWA disabled — unregisters any previously installed service worker.\n" + "self.addEventListener('install', () => self.skipWaiting());\n" + "self.addEventListener('activate', event => {\n" + " event.waitUntil(self.registration.unregister());\n" + "});\n", + encoding="utf-8", + ) + manifest = site_path / "manifest.json" + if manifest.exists(): + manifest.unlink() + click.echo(" Removed manifest.json (pwa: no)") + click.echo(" Wrote service-worker.js (self-unregistering stub, pwa: no)") + + def generate_pwa(site_path: Path, cfg: dict): """Generate manifest.json and service-worker.js when pwa: yes.""" pwa_name = cfg.get("pwa-name", cfg.get("sitename", "MD-CMS Site"))