Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
dist/
# generated types
.astro/
# `npx wrangler dev` 的本機狀態。本機不安裝 wrangler(見 wrangler.jsonc 開頭),
# 但要親眼確認未匹配路徑真的拿得到 404 頁時,只有它跑得起 Workers 的資產路由。
.wrangler/

# 字體子集化的原始檔快取(scripts/subset-fonts.py 會自行下載)
scripts/.fontcache/
Expand Down
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,19 @@ Cloudflare 建置:`npm run build` → `npx wrangler deploy`。建置設定在
- `wrangler.jsonc` 供 `npx wrangler deploy` 讀取,在 Cloudflare 的建置環境執行。
本機不需要 wrangler,因此不在 dependencies,也沒有 `deploy` script。
- 本站不落在任何節點上:不進 hoshi-deploy inventory,origin 憑證不含 `hoshivel.com`。
- 未匹配的路徑由 `assets.not_found_handling` 交給 `dist/404.html`。這個鍵的預設值
`"none"` 回的是**零位元組**的 404——`src/pages/404.astro` 存在、`astro build`
也產得出 `dist/404.html`,兩者都不代表它被服務。要親眼確認就在本機跑一次
Workers 的資產路由(一次性,不加進 dependencies):

```sh
npx astro build
npx wrangler dev --port 26829
curl -sS -i http://127.0.0.1:26829/zzz-not-here
```

要看到 `404` 後面**接著一份 HTML**。空白的 body 會被瀏覽器畫成白頁、分頁標題
退回原始 URL,看起來像整個站掛了,而不是路徑打錯。

`hoshi build` 產生 `dist/` 靜態檔。**不得**加入 SSR adapter、server/hybrid
output、Node runtime 或容器產物——部署的是純靜態資產。
Expand Down
99 changes: 99 additions & 0 deletions test/not-found.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/*
The 404 page only exists if something serves it.

`src/pages/404.astro` was here from early on and `astro build` has always
written `dist/404.html`, yet every unmatched path on hoshivel.com returned a
404 with a ZERO-BYTE body until 2026-09-03. The gap was one key in
`wrangler.jsonc`: `assets.not_found_handling` defaults to `"none"`, which
answers unmatched requests with an empty response and never opens `404.html`.

A browser paints an empty body as a blank white page and falls back to the raw
URL for the tab title, so the failure looks like a crashed site rather than a
typo. The sister site sr-web had the identical defect and the identical fix;
see the workspace decision
`decisions/infrastructure/Workers-靜態站要顯式開啟-404-頁.md`.

Nothing in the build catches this: the page compiles, the asset is emitted, the
deploy succeeds. Only these assertions stand between the config and a silent
return to the blank page.
*/
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import test from "node:test";

import { ui as dictionaries } from "../src/i18n/ui.ts";

const read = (path) => readFileSync(new URL(path, import.meta.url), "utf8");

/**
* Strip JSONC comments without touching comment-like text inside strings, so a
* value such as an "https://" URL cannot truncate the parse.
*/
const parseJsonc = (source) => {
let out = "";
let inString = false;
let escaped = false;
for (let i = 0; i < source.length; i += 1) {
const ch = source[i];
if (inString) {
out += ch;
if (escaped) escaped = false;
else if (ch === "\\") escaped = true;
else if (ch === '"') inString = false;
continue;
}
if (ch === '"') {
inString = true;
out += ch;
continue;
}
if (ch === "/" && source[i + 1] === "/") {
while (i < source.length && source[i] !== "\n") i += 1;
out += "\n";
continue;
}
if (ch === "/" && source[i + 1] === "*") {
i += 2;
while (i < source.length && !(source[i] === "*" && source[i + 1] === "/")) i += 1;
i += 1;
continue;
}
out += ch;
}
return JSON.parse(out);
};

const wrangler = parseJsonc(read("../wrangler.jsonc"));
const astroConfig = read("../astro.config.mjs");
const page = read("../src/pages/404.astro");

test("unmatched paths are answered with the 404 page, not an empty body", () => {
assert.equal(
wrangler.assets?.not_found_handling,
"404-page",
'assets.not_found_handling must be "404-page"; the default "none" returns a zero-byte 404',
);
});

test("the asset directory Workers serves is the one Astro builds into", () => {
// The 404 wiring is worth nothing if it points at a directory the build never
// fills. Astro's outDir is left at its default here, so `dist` is the contract
// between the two files — assert the config has not quietly moved.
assert.match(wrangler.assets?.directory ?? "", /^\.\/dist\/?$/);
assert.doesNotMatch(astroConfig, /outDir/, "astro outDir moved; wrangler assets.directory must follow");
});

test("the 404 page draws its copy from the shared dictionary", () => {
// Hardcoded strings here drift away from src/i18n/ui.ts on the next copy pass,
// and this page is the one nobody opens on purpose.
for (const key of ["notfound.title", "notfound.body", "notfound.back"]) {
assert.ok(page.includes(`"${key}"`), `404.astro must use ${key}`);
for (const [locale, dictionary] of Object.entries(dictionaries)) {
assert.ok(dictionary[key]?.trim(), `${locale}/${key} must not be empty`);
}
}
});

test("the 404 page links back into the site", () => {
assert.match(page, /href="\/"/, "404.astro must offer a link home");
});
8 changes: 7 additions & 1 deletion wrangler.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@
"name": "hoshivel-web",
"compatibility_date": "2026-08-10",
"assets": {
"directory": "./dist"
"directory": "./dist",
// 未匹配的路徑要拿到 `dist/404.html`。這個鍵的預設值是 `"none"`,而 `"none"`
// 回的是一份**零位元組**的 404:瀏覽器把空 body 畫成白頁、分頁標題退回原始
// URL,看起來就像站掛了。`src/pages/404.astro` 在、`dist/404.html` 也一直
// 建置得出來,缺的只有這一行——三者單獨看都正常,症狀卻指向別處。
// 理由見 workspace 的 decisions/infrastructure/Workers-靜態站要顯式開啟-404-頁.md。
"not_found_handling": "404-page"
}
}