Public: anyone can read this; the crew can change it.
/favicon.ico and /apple-touch-icon.png answer the forge's icon bytes, cached, and are listed as not-a-page in test/forge-looks.test.ts #8221
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/7757-favicon-ico-and-apple-touch-icon-png-ans → main 3 commits fork
6 files
changed +65
−1
src/core/reserved.ts +3
| @@ -93,6 +93,9 @@ export const SYSTEM_NAMES: readonly string[] = [ | ||
| 93 | 93 | "resilience", |
| 94 | 94 | "origins", |
| 95 | 95 | "robots.txt", |
| 96 | "favicon.ico", | |
| 97 | "apple-touch-icon.png", | |
| 98 | "apple-touch-icon-precomposed.png", | |
| 96 | 99 | "llms.txt", |
| 97 | 100 | "runs", |
| 98 | 101 | "search", |
src/http/server.ts +3
| @@ -190,6 +190,7 @@ import { ceilingAct, grantAct, modelAdd, modelPage } from "../web/pages/model.ts | ||
| 190 | 190 | import { |
| 191 | 191 | changePassword, |
| 192 | 192 | iconPng, |
| 193 | appleTouchIcon, | |
| 193 | 194 | favicon, |
| 194 | 195 | llmsTxt, |
| 195 | 196 | releasesJson, |
| @@ -717,6 +718,8 @@ export function buildRouter(): Router<Handler> { | ||
| 717 | 718 | router.add("GET", "/opensearch.xml", openSearch); |
| 718 | 719 | router.add("GET", "/icons/:spec.png", iconPng); |
| 719 | 720 | router.add("GET", "/favicon.ico", favicon); |
| 721 | router.add("GET", "/apple-touch-icon.png", appleTouchIcon); | |
| 722 | router.add("GET", "/apple-touch-icon-precomposed.png", appleTouchIcon); | |
| 720 | 723 | // The highlighter bundle, content-addressed. A stale hash is a 404 rather |
| 721 | 724 | // than a redirect: the page that asked for it names the hash it was built |
| 722 | 725 | // with, so a miss means an old tab, and an old tab should reload. |
src/web/standards.ts +8
| @@ -303,6 +303,14 @@ function pngResponse(png: Uint8Array): Response { | ||
| 303 | 303 | }); |
| 304 | 304 | } |
| 305 | 305 | |
| 306 | /** The icon a browser, a home screen or an unfurler asks for by habit. */ | |
| 307 | function fixedIcon(spec: string) { | |
| 308 | return (ctx: Ctx): Response => iconPng({ ...ctx, params: { ...ctx.params, spec } }); | |
| 309 | } | |
| 310 | ||
| 311 | /** GET /apple-touch-icon.png and the -precomposed twin — the 180 icon. */ | |
| 312 | export const appleTouchIcon = fixedIcon("forge-180"); | |
| 313 | ||
| 306 | 314 | /** `Running 0.7.51 at a1b2c3d`: which build these instructions describe. */ |
| 307 | 315 | function runningSentence(): string { |
| 308 | 316 | const r = running(); |
test/design-audit.test.ts +1 −1
| @@ -11,7 +11,7 @@ const doc = readFileSync("docs/design/audit.md", "utf8"); | ||
| 11 | 11 | |
| 12 | 12 | /** Routes that answer a file, a fragment, a redirect hop or a callback. */ |
| 13 | 13 | const NOT_PAGES = |
| 14 | /^\/(assets\/|\.well-known\/|icons\/|og\/|live\/|__forge\/|go\/|events$|system\/stream$|settings\/teams\/stream$|sw\.js$|robots\.txt$|favicon\.ico$|sitemap\.xml$|manifest\.webmanifest$|opensearch\.xml$|llms\.txt$|releases\.json$|audit\.csv$|fleet\/graph\/live$|fleet\/graph\.csv$|settings\/origins\/callback$|home\/archived\/|settings\/backups\/live$)/; | |
| 14 | /^\/(assets\/|\.well-known\/|icons\/|og\/|live\/|__forge\/|go\/|events$|system\/stream$|settings\/teams\/stream$|sw\.js$|robots\.txt$|favicon\.ico$|apple-touch-icon(-precomposed)?\.png$|sitemap\.xml$|manifest\.webmanifest$|opensearch\.xml$|llms\.txt$|releases\.json$|audit\.csv$|fleet\/graph\/live$|fleet\/graph\.csv$|settings\/origins\/callback$|home\/archived\/|settings\/backups\/live$)/; | |
| 15 | 15 | |
| 16 | 16 | const getRoutes = [ |
| 17 | 17 | ...server.matchAll(/router\.add\(\s*"GET",\s*"([^"]+)"/g), |
test/favicon.test.ts added +48
| @@ -0,0 +1,48 @@ | ||
| 1 | /** The icons a browser, a home screen and an unfurler ask for by habit. */ | |
| 2 | ||
| 3 | import { afterAll, beforeAll, expect, test } from "bun:test"; | |
| 4 | import { mkdtemp, rm } from "node:fs/promises"; | |
| 5 | import { tmpdir } from "node:os"; | |
| 6 | import { join } from "node:path"; | |
| 7 | import { createApp, type App } from "../src/app.ts"; | |
| 8 | import { serve, type Servers } from "../src/http/server.ts"; | |
| 9 | ||
| 10 | let dir: string; | |
| 11 | let app: App; | |
| 12 | let servers: Servers; | |
| 13 | ||
| 14 | beforeAll(async () => { | |
| 15 | dir = await mkdtemp(join(tmpdir(), "forge-favicon-")); | |
| 16 | app = createApp({ dataDir: dir, port: 0 }); | |
| 17 | servers = await serve(app); | |
| 18 | }); | |
| 19 | ||
| 20 | afterAll(async () => { | |
| 21 | servers.stop(); | |
| 22 | app.close(); | |
| 23 | await rm(dir, { recursive: true, force: true }); | |
| 24 | }); | |
| 25 | ||
| 26 | const CASES: [string, string][] = [ | |
| 27 | ["/favicon.ico", "/icons/forge-32.png"], | |
| 28 | ["/apple-touch-icon.png", "/icons/forge-180.png"], | |
| 29 | ["/apple-touch-icon-precomposed.png", "/icons/forge-180.png"], | |
| 30 | ]; | |
| 31 | ||
| 32 | for (const [path, twin] of CASES) { | |
| 33 | test(`${path} answers the bytes of ${twin}, cached`, async () => { | |
| 34 | const r = await fetch(`${servers.url}${path}`); | |
| 35 | const want = await fetch(`${servers.url}${twin}`); | |
| 36 | expect(r.status).toBe(200); | |
| 37 | expect(r.headers.get("content-type")).toBe("image/png"); | |
| 38 | expect(r.headers.get("cache-control")).toBe(want.headers.get("cache-control") ?? "x"); | |
| 39 | expect(new Uint8Array(await r.arrayBuffer())).toEqual(new Uint8Array(await want.arrayBuffer())); | |
| 40 | }); | |
| 41 | ||
| 42 | test(`HEAD ${path} has no body`, async () => { | |
| 43 | const r = await fetch(`${servers.url}${path}`, { method: "HEAD" }); | |
| 44 | expect(r.status).toBe(200); | |
| 45 | expect(r.headers.get("content-type")).toBe("image/png"); | |
| 46 | expect((await r.arrayBuffer()).byteLength).toBe(0); | |
| 47 | }); | |
| 48 | } |
test/forge-looks.test.ts +2
| @@ -45,6 +45,8 @@ const NOT_A_PAGE: Record<string, string> = { | ||
| 45 | 45 | "/sign-in": "redirects to /signin", |
| 46 | 46 | "/mcp": "a protocol endpoint, answers 405 to a GET", |
| 47 | 47 | "/_measure": "the page-load beacon, a POST endpoint", |
| 48 | "/apple-touch-icon.png": "an icon, not a page", | |
| 49 | "/apple-touch-icon-precomposed.png": "an icon, not a page", | |
| 48 | 50 | "/events": "a live event stream that never ends", |
| 49 | 51 | }; |
| 50 | 52 |
new above
offline