spa.js treats host 404-fallback (200 + index.html) as successful navigation — broken links silently render the homepage at a phantom URL; build emits no 404.html #73
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
When a static
zetl buildis deployed to a host with SPA-style 404 fallback (e.g. Cloudflare Pages with no404.htmlin the output),spa.jsnavigate()treats the fallback response as a successful navigation: any broken/stale link fetches the homepage HTML with HTTP 200,swap()renders it, andpushStateleaves the phantom URL in the address bar. The user silently gets the wrong page at a wrong URL instead of any visible failure.Two zetl-side gaps combine to cause this:
spa.jsnavigate()only checksr.ok— it never verifies that the document it fetched is actually the page it asked for.zetl buildemits no404.html— Cloudflare Pages (and similar hosts) interpret its absence as "SPA mode" and serve/index.htmlwith 200 for every unknown path, so!r.okcan never trip.Follow-up to #70: this is what made #70 so confusing to diagnose in production. On a fallback host the compounded URLs never visibly 404 — navigation just "goes to the homepage" at URLs like
/papers/concepts/<slug>/index.html, with no error in console or UI. Any future bug (or plain dead link —zetl statsalready countsdead_links) gets the same silent masking.Reproduction (live, zetl 0.9.0 build on Cloudflare Pages)
Observed on https://agent-comms.anuna.io (built pre-#70-fix, so stale sidebar links provide the broken hrefs; but any dead link reproduces the masking on a current build):
https://agent-comms.anuna.io/concepts/adaptive-protocols/./papers/security/security-applications-of-formal-language-theory/index.html(correct)./papers/concepts/adaptive-protocols/index.html— a path that does not exist.fetch()gets the Cloudflare Pages fallback: 200 with the homepage document (<title>index — Agent Communications</title>).swap()renders the homepage,pushStatekeeps the phantom URL.Playwright trace of step 3–4:
Expected
zetl buildshould emit a404.htmlso hosts like Cloudflare Pages don't switch to SPA fallback mode.Suggested fix
navigate()/swap(), verify the fetched document matches the requested slug before swapping — e.g. compare the parsed doc'sdata-slug(or a<link rel="canonical">/<meta name="zetl:slug">emitted per page) against the target slug derived fromurl; on mismatch,location.href = urlinstead of swapping.dist/404.htmlinzetl build(rendered with the shell, absolute asset URLs since it can be served at any depth).Either alone helps; both together close the hole (1 protects SPA nav on hosts that still 200-fallback, 2 restores honest status codes for full loads and crawlers).
Environment
navigate()inthemes/default/static/spa.jsat HEAD (v0.9.2,830638a) still has the sameif (!r.ok) throw r.statuscheck only, andzetl buildat HEAD still emits no404.html.404.htmlin upload → automatic index.html fallback with 200).