diff --git a/docs/setup.md b/docs/setup.md index 61c0c4b..ca3b2ae 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -93,8 +93,9 @@ Account **`830ade508fd3f90a2a591477cdbd399c`** (Kinseycagney), zone `nbread.lol` `0x4AAAAAAD1_vRxPvgk56oO-`, domain `nbread.lol`. `TURNSTILE_SECRET_KEY` set via `wrangler secret put`. (`ADMIN_PUBKEY` unset ⇒ `/admin` 404s — optional.) - **DNS** (proxied): apex `A @ 192.0.2.1`, apex `AAAA @ 100::`, wildcard - `CNAME * → nbread.lol`. `www` left on the registrar CNAME (`pixie.porkbun.com`) - — follow-up: repoint or add a `www → apex` redirect. + `CNAME * → nbread.lol`, `www CNAME → nbread.lol` (all proxied). `www.nbread.lol` + 301-redirects to the apex in the Worker (guard middleware; "www" is a reserved + handle, never a blog). - **SSL/TLS**: Full (strict); Universal SSL active covering `nbread.lol` + `*.nbread.lol`; Always Use HTTPS **on**; HSTS `max-age=15552000` (no `includeSubDomains`/preload); TLS 1.3 **on**. @@ -112,8 +113,6 @@ Account **`830ade508fd3f90a2a591477cdbd399c`** (Kinseycagney), zone `nbread.lol` - **`ADMIN_PUBKEY`** unset — set via `wrangler secret put` to enable the `/admin` blocklist surface (unset ⇒ all `/admin` 404, which smoke asserts). -- **`www.nbread.lol`** still CNAMEs to the registrar parking host — repoint or - add a `www → apex` redirect if wanted. The old `nostrbook` worker / `nostrbook.net` zone keep serving until explicitly retired — decommission is a separate step. diff --git a/src/middleware/guard.ts b/src/middleware/guard.ts index 4a32747..ef6257f 100644 --- a/src/middleware/guard.ts +++ b/src/middleware/guard.ts @@ -73,6 +73,16 @@ export const guard: MiddlewareHandler = async (c, next) => { } const main = c.env.MAIN_HOST.toLowerCase(); + // Canonicalize www → apex: www.
permanently redirects to the apex, + // preserving path + query. "www" is a reserved handle (never a blog), so the + // host carries no tenant meaning — sending it to the canonical apex avoids a + // dead 404 and keeps a single canonical origin. Runs before subdomain + // classification so www never reaches the tenant D1 lookup. + if (hostname === "www." + main) { + const url = new URL(c.req.url); + return c.redirect(`https://${main}${url.pathname}${url.search}`, 301); + } + if (LOOPBACK_HOSTS.has(hostname)) { // wrangler dev convenience: treat the loopback host as the apex. c.set("host", main); diff --git a/test/integration/headers.spec.ts b/test/integration/headers.spec.ts index db41f4e..c4719ee 100644 --- a/test/integration/headers.spec.ts +++ b/test/integration/headers.spec.ts @@ -127,6 +127,22 @@ describe("apex class", () => { expectApexClass(res); }); + it("www subdomain 301-redirects to the apex, preserving path + query", async () => { + const root = await SELF.fetch("https://www.nbread.lol/", { + redirect: "manual", + }); + expect(root.status).toBe(301); + expect(root.headers.get("location")).toBe("https://nbread.lol/"); + + const deep = await SELF.fetch("https://www.nbread.lol/discover?q=hi", { + redirect: "manual", + }); + expect(deep.status).toBe(301); + expect(deep.headers.get("location")).toBe( + "https://nbread.lol/discover?q=hi", + ); + }); + it("/discover carries apex headers on miss AND on cache hit", async () => { const miss = await SELF.fetch("https://nbread.lol/discover"); expect(miss.status).toBe(200);