Public: anyone can read this; the crew can change it.
Every page that exists names its canonical address, equal to its og:url #3101
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/3090-every-page-that-exists-names-its-canonic → main 1 commit fork
2 files
changed +39
−4
src/web/layout.ts +7 −3
| @@ -590,9 +590,13 @@ export function page(options: PageOptions, status = 200): Response { | ||
| 590 | 590 | // the same bytes, or the difference tells a guesser something. |
| 591 | 591 | status === 200 |
| 592 | 592 | ? html`<meta |
| 593 | property="og:url" | |
| 594 | content="${ctx.url.origin}${ctx.url.pathname}" | |
| 595 | />` | |
| 593 | property="og:url" | |
| 594 | content="${ctx.url.origin}${ctx.url.pathname}" | |
| 595 | /> | |
| 596 | <link | |
| 597 | rel="canonical" | |
| 598 | href="${ctx.url.origin}${ctx.url.pathname}" | |
| 599 | />` | |
| 596 | 600 | : null |
| 597 | 601 | } |
| 598 | 602 | <meta |
test/forge-webspec.test.ts +32 −1
| @@ -8,6 +8,8 @@ import { mkdtemp, rm } from "node:fs/promises"; | ||
| 8 | 8 | import { tmpdir } from "node:os"; |
| 9 | 9 | import { join } from "node:path"; |
| 10 | 10 | import { createApp, type App } from "../src/app.ts"; |
| 11 | import { commitToBranch, createRepo } from "../src/actions/repos.ts"; | |
| 12 | import { createUser } from "../src/core/users.ts"; | |
| 11 | 13 | import { forgeHttp, type ForgeHttp } from "../src/http/server.ts"; |
| 12 | 14 | import { webspec } from "../src/release/webspec.ts"; |
| 13 | 15 | import { quietLogs } from "./helpers/globals.ts"; |
| @@ -23,7 +25,6 @@ const PATHS = ["/", "/signin", "/signup", "/explore"]; | ||
| 23 | 25 | */ |
| 24 | 26 | const KNOWN: Record<string, [string, string]> = {}; |
| 25 | 27 | const WHY: Record<string, [string, string]> = { |
| 26 | canonical: ["https://specification.website/#canonical", "no page emits <link rel=canonical>; one origin serves each page, so nothing duplicates yet"], | |
| 27 | 28 | "color-scheme": ["https://specification.website/#color-scheme", "pages carry no color-scheme meta; the stylesheet picks the theme"], |
| 28 | 29 | compression: ["https://specification.website/", "bodies leave the forge uncompressed: compression is set at the edge (edge: true)"], |
| 29 | 30 | "permissions-policy": ["https://specification.website/#permissions-policy", "the security floor sets CSP, nosniff and Referrer-Policy but no Permissions-Policy"], |
| @@ -57,3 +58,33 @@ test("the forge's signed-out pages raise exactly the findings named in KNOWN", a | ||
| 57 | 58 | const seen = new Set(found.map(keyOf)); |
| 58 | 59 | expect(Object.keys(KNOWN).filter((k) => !seen.has(k))).toEqual([]); |
| 59 | 60 | }); |
| 61 | ||
| 62 | test("a page that answers 200 names its canonical address, equal to og:url; a 404 names none", async () => { | |
| 63 | const made = await createUser(app.db, { | |
| 64 | origin: "seed", | |
| 65 | trusted: true, | |
| 66 | username: "alice", | |
| 67 | email: "[email protected]", | |
| 68 | password: "a-good-passphrase", | |
| 69 | adminIfFirst: true, | |
| 70 | }); | |
| 71 | if (!made.ok) throw new Error("user"); | |
| 72 | const repo = await createRepo(app, made.value, { name: "notes", visibility: "public" }); | |
| 73 | if (!repo.ok) throw new Error("repo"); | |
| 74 | await commitToBranch(app, made.value, repo.value, { message: "seed", files: [{ path: "a.txt", content: "a" }] }); | |
| 75 | const head = async (path: string) => { | |
| 76 | const res = await http.handle(new Request(`http://forge.test${path}`)); | |
| 77 | return { status: res.status, html: await res.text() }; | |
| 78 | }; | |
| 79 | for (const path of ["/", "/explore", "/alice/notes", "/explore?x=1"]) { | |
| 80 | const { status, html } = await head(path); | |
| 81 | expect(status).toBe(200); | |
| 82 | const url = html.match(/<meta\s+property="og:url"\s+content="([^"]*)"/)?.[1]; | |
| 83 | const canonical = html.match(/<link\s+rel="canonical"\s+href="([^"]*)"/)?.[1]; | |
| 84 | expect(url).toBe(`http://forge.test${path.split("?")[0]}`); | |
| 85 | expect(canonical).toBe(url); | |
| 86 | } | |
| 87 | const missing = await head("/no-such-page-here"); | |
| 88 | expect(missing.status).toBe(404); | |
| 89 | expect(missing.html).not.toContain('rel="canonical"'); | |
| 90 | }); |
new above
offline