v3.8.1 — Two production 404s (site root, consent banner) + link crawler
Two live 404s that a browser never shows you, found by driving the deployed site instead of reading source.
Includes everything in v3.8.0 — cookie prefix fix (#120) and mobile UX. Deploying still logs every admin out once; announce before rolling out.
🔴 The site root 404'd for Accept-Language: *
GET / returned 404 to any client sending the wildcard — including Node's fetch, which sends it by default. curl worked, so the site looked healthy from a terminal.
curl -sI https://…/ → Location: /en ✅
node -e 'fetch("https://…/")' → 404 at /* ❌
The guard was inert:
if (lang && SUPPORTED_LOCALES.includes(toLocale(lang)))
throw redirect(308, `/${lang}`); // ← raw tag, not the validated onetoLocale() coerces anything unknown to DEFAULT_LOCALE, so includes(toLocale(x)) is true for every input — then the redirect used the raw tag:
Accept-Language |
Guard | Redirects to | |
|---|---|---|---|
en-US |
true | /en |
✅ |
* |
true | /* |
❌ 404 |
zz |
true | /zz |
❌ 404 |
* is legal per RFC 9110 §12.5.4, so this was real traffic — uptime monitors, crawlers, and any client not sending a specific language got a 404 homepage. If you run synthetic monitoring against /, check whether it has been silently failing.
🔴 The consent banner linked to a 404
The privacy-policy href was hardcoded, so any install without a page at that slug shipped a broken link on its GDPR consent banner — the one link there that legally ought to resolve.
The layout now looks the page up and passes an href only when a published page exists; the banner omits the link otherwise. Looked up rather than configured, so it self-corrects when you publish or unpublish — no redeploy. A draft policy deliberately doesn't count.
Check your own install: if you have no published page at /privacy-policy, that link has been 404ing.
🔧 Link crawler
node scripts/smoke-links.mjs https://your-site.exampleCrawls a deployed install and exits non-zero on any broken internal link. It found the first defect and independently reproduced the second in both locales across 17 pages.
Unit tests don't follow hrefs — only a crawler finds "this link points at nothing". Worth wiring into your deploy pipeline.
📐 163-character lines
At 1920px the banner copy ran the full viewport width — measured 163ch against the 45–75ch typographic guideline. Past ~90ch the eye loses its place returning to the next line: the worst property to give text asking for a privacy decision.
🚀 Upgrading
git fetch upstream && git merge upstream/mainNo migration, no config change. Announce the forced logout (from v3.8.0's cookie change) and deploy.
Setup instructions — what belongs at /admin/settings/secrets vs the Cloudflare dashboard — are unchanged from v3.7.1.
Tests
121 (was 109). Locale and consent-banner guards are mutation-verified; the locale test keeps the original defective predicate alongside the fix so the failure mode stays documented.
Full changelog: v3.8.0...v3.8.1