|
| 1 | +/* |
| 2 | + The 404 page only exists if something serves it. |
| 3 | +
|
| 4 | + `src/pages/404.astro` was here from early on and `astro build` has always |
| 5 | + written `dist/404.html`, yet every unmatched path on hoshivel.com returned a |
| 6 | + 404 with a ZERO-BYTE body until 2026-09-03. The gap was one key in |
| 7 | + `wrangler.jsonc`: `assets.not_found_handling` defaults to `"none"`, which |
| 8 | + answers unmatched requests with an empty response and never opens `404.html`. |
| 9 | +
|
| 10 | + A browser paints an empty body as a blank white page and falls back to the raw |
| 11 | + URL for the tab title, so the failure looks like a crashed site rather than a |
| 12 | + typo. The sister site sr-web had the identical defect and the identical fix; |
| 13 | + see the workspace decision |
| 14 | + `decisions/infrastructure/Workers-靜態站要顯式開啟-404-頁.md`. |
| 15 | +
|
| 16 | + Nothing in the build catches this: the page compiles, the asset is emitted, the |
| 17 | + deploy succeeds. Only these assertions stand between the config and a silent |
| 18 | + return to the blank page. |
| 19 | +*/ |
| 20 | +import assert from "node:assert/strict"; |
| 21 | +import { readFileSync } from "node:fs"; |
| 22 | +import test from "node:test"; |
| 23 | + |
| 24 | +import { ui as dictionaries } from "../src/i18n/ui.ts"; |
| 25 | + |
| 26 | +const read = (path) => readFileSync(new URL(path, import.meta.url), "utf8"); |
| 27 | + |
| 28 | +/** |
| 29 | + * Strip JSONC comments without touching comment-like text inside strings, so a |
| 30 | + * value such as an "https://" URL cannot truncate the parse. |
| 31 | + */ |
| 32 | +const parseJsonc = (source) => { |
| 33 | + let out = ""; |
| 34 | + let inString = false; |
| 35 | + let escaped = false; |
| 36 | + for (let i = 0; i < source.length; i += 1) { |
| 37 | + const ch = source[i]; |
| 38 | + if (inString) { |
| 39 | + out += ch; |
| 40 | + if (escaped) escaped = false; |
| 41 | + else if (ch === "\\") escaped = true; |
| 42 | + else if (ch === '"') inString = false; |
| 43 | + continue; |
| 44 | + } |
| 45 | + if (ch === '"') { |
| 46 | + inString = true; |
| 47 | + out += ch; |
| 48 | + continue; |
| 49 | + } |
| 50 | + if (ch === "/" && source[i + 1] === "/") { |
| 51 | + while (i < source.length && source[i] !== "\n") i += 1; |
| 52 | + out += "\n"; |
| 53 | + continue; |
| 54 | + } |
| 55 | + if (ch === "/" && source[i + 1] === "*") { |
| 56 | + i += 2; |
| 57 | + while (i < source.length && !(source[i] === "*" && source[i + 1] === "/")) i += 1; |
| 58 | + i += 1; |
| 59 | + continue; |
| 60 | + } |
| 61 | + out += ch; |
| 62 | + } |
| 63 | + return JSON.parse(out); |
| 64 | +}; |
| 65 | + |
| 66 | +const wrangler = parseJsonc(read("../wrangler.jsonc")); |
| 67 | +const astroConfig = read("../astro.config.mjs"); |
| 68 | +const page = read("../src/pages/404.astro"); |
| 69 | + |
| 70 | +test("unmatched paths are answered with the 404 page, not an empty body", () => { |
| 71 | + assert.equal( |
| 72 | + wrangler.assets?.not_found_handling, |
| 73 | + "404-page", |
| 74 | + 'assets.not_found_handling must be "404-page"; the default "none" returns a zero-byte 404', |
| 75 | + ); |
| 76 | +}); |
| 77 | + |
| 78 | +test("the asset directory Workers serves is the one Astro builds into", () => { |
| 79 | + // The 404 wiring is worth nothing if it points at a directory the build never |
| 80 | + // fills. Astro's outDir is left at its default here, so `dist` is the contract |
| 81 | + // between the two files — assert the config has not quietly moved. |
| 82 | + assert.match(wrangler.assets?.directory ?? "", /^\.\/dist\/?$/); |
| 83 | + assert.doesNotMatch(astroConfig, /outDir/, "astro outDir moved; wrangler assets.directory must follow"); |
| 84 | +}); |
| 85 | + |
| 86 | +test("the 404 page draws its copy from the shared dictionary", () => { |
| 87 | + // Hardcoded strings here drift away from src/i18n/ui.ts on the next copy pass, |
| 88 | + // and this page is the one nobody opens on purpose. |
| 89 | + for (const key of ["notfound.title", "notfound.body", "notfound.back"]) { |
| 90 | + assert.ok(page.includes(`"${key}"`), `404.astro must use ${key}`); |
| 91 | + for (const [locale, dictionary] of Object.entries(dictionaries)) { |
| 92 | + assert.ok(dictionary[key]?.trim(), `${locale}/${key} must not be empty`); |
| 93 | + } |
| 94 | + } |
| 95 | +}); |
| 96 | + |
| 97 | +test("the 404 page links back into the site", () => { |
| 98 | + assert.match(page, /href="\/"/, "404.astro must offer a link home"); |
| 99 | +}); |
0 commit comments