Public: anyone can read this; the crew can change it.

Every page carries a tab icon, and /favicon.ico answers with the mark instead of a 404 #7919

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

torque/forge:torque/7914-every-page-carries-a-tab-icon-and-favico → main 4 commits fork 2 threads · 2 unresolved

  • Merged. torque/7914-every-page-carries-a-tab-icon-and-favico was merged into main as ed601e4.

Review

Sign in to review this change.
6 files changed +38 −3
src/core/reserved.ts +1
@@ -68,6 +68,7 @@ export const SYSTEM_NAMES: readonly string[] = [
6868 "events",
6969 "explore",
7070 "family",
71 "favicon.ico",
7172 "fleet",
7273 "flow",
7374 "front",
src/http/server.ts +2
@@ -189,6 +189,7 @@ import { ceilingAct, grantAct, modelAdd, modelPage } from "../web/pages/model.ts
189189 import {
190190 changePassword,
191191 iconPng,
192 favicon,
192193 llmsTxt,
193194 releasesJson,
194195 robotsTxt,
@@ -714,6 +715,7 @@ export function buildRouter(): Router<Handler> {
714715 router.add("GET", "/manifest.webmanifest", webManifest);
715716 router.add("GET", "/opensearch.xml", openSearch);
716717 router.add("GET", "/icons/:spec.png", iconPng);
718 router.add("GET", "/favicon.ico", favicon);
717719 // The highlighter bundle, content-addressed. A stale hash is a 404 rather
718720 // than a redirect: the page that asked for it names the hash it was built
719721 // with, so a miss means an old tab, and an old tab should reload.
src/web/standards.ts +16 −2
@@ -16,6 +16,7 @@
1616 * report a problem without an account, how
1717 * to offer a change (llmstxt.org)
1818 * /icons/forge-<size>.png the mark, rastered for the manifest and iOS
19 * /favicon.ico the 32px mark, for a browser that asks by habit
1920 *
2021 * Nothing here is configured; everything is derived from what the forge
2122 * already knows — its URL, its first account, its public repositories, its
@@ -267,7 +268,7 @@ export function webManifest(ctx: Ctx): Response {
267268 );
268269 }
269270
270 const SIZES = new Set([180, 192, 512]);
271 const SIZES = new Set([32, 180, 192, 512]);
271272
272273 /** GET /icons/forge-:spec.png — `<size>` or `<size>-maskable`. */
273274 export function iconPng(ctx: Ctx): Response {
@@ -279,7 +280,20 @@ export function iconPng(ctx: Ctx): Response {
279280 // iOS rounds its own corners, so the touch icon is painted edge to edge
280281 // like a maskable one.
281282 const shape: MarkShape = m[2] !== undefined || size === 180 ? "full" : "icon";
282 const png = markPng(size, shape);
283 return pngResponse(markPng(size, shape));
284 }
285
286 /**
287 * GET /favicon.ico — the mark at 32px (`/icons/forge-32.png`). Every page
288 * carries its own inline SVG icon, but a browser asks here anyway on a page
289 * that is not ours (a raw file, a JSON answer); a PNG under an .ico name is
290 * read by every browser that asks, and answering spares the console a 404.
291 */
292 export function favicon(): Response {
293 return pngResponse(markPng(32, "icon"));
294 }
295
296 function pngResponse(png: Uint8Array): Response {
283297 return new Response(png, {
284298 headers: {
285299 "content-type": "image/png",
test/design-audit.test.ts +1 −1
@@ -11,7 +11,7 @@ const doc = readFileSync("docs/design/audit.md", "utf8");
1111
1212 /** Routes that answer a file, a fragment, a redirect hop or a callback. */
1313 const NOT_PAGES =
14 /^\/(assets\/|\.well-known\/|icons\/|og\/|live\/|__forge\/|go\/|events$|system\/stream$|settings\/teams\/stream$|sw\.js$|robots\.txt$|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$|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$)/;
1515
1616 const getRoutes = [
1717 ...server.matchAll(/router\.add\(\s*"GET",\s*"([^"]+)"/g),
test/forge-looks.test.ts +1
@@ -22,6 +22,7 @@ quietLogs();
2222 const NOT_A_PAGE: Record<string, string> = {
2323 "/llms.txt": "plain markdown for machines, not a page to picture",
2424 "/robots.txt": "plain text for crawlers",
25 "/favicon.ico": "the tab icon, an image",
2526 "/releases.json": "JSON feed of /releases",
2627 "/sitemap.xml": "XML for crawlers",
2728 "/manifest.webmanifest": "the install manifest, JSON",
test/standards.test.ts +17
@@ -345,6 +345,23 @@ describe("what the open web expects", () => {
345345 expect((await fetch(`${base}/icons/forge-64.png`)).status).toBe(404);
346346 });
347347
348 test("/favicon.ico answers with the 32px mark instead of a 404", async () => {
349 const icon = await fetch(`${base}/icons/forge-32.png`);
outdated

Drop the head-link assertion along with the layout link. Keep the /favicon.ico checks.

350 const fav = await fetch(`${base}/favicon.ico`);
351 expect(fav.status).toBe(200);
352 expect(fav.headers.get("content-type")).toBe("image/png");
353 expect(fav.headers.get("cache-control")).toBe(icon.headers.get("cache-control"));
354 expect(fav.headers.get("cache-control")).toContain("immutable");
355 const bytes = new Uint8Array(await fav.arrayBuffer());
356 expect(bytes).toEqual(new Uint8Array(await icon.arrayBuffer()));
357 const header = pngHeader(bytes);
358 expect(header.ok && `${header.value.width}x${header.value.height}`).toBe("32x32");
359 // The security floor is on it like on every response.
360 expect(fav.headers.get("x-content-type-options")).toBe("nosniff");
361 expect(fav.headers.get("referrer-policy")).not.toBeNull();
362 expect(fav.headers.get("content-security-policy")).not.toBeNull();
363 });
364
348365 test("the mark is painted, not pasted: gradient inside, nothing outside", () => {
349366 const decoded = decodePng(markPng(64, "icon"));
350367 expect(decoded.ok).toBe(true);

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs