Add an SVG favicon (no more /favicon.ico 404)

Link a small inline SVG (site/favicon.svg, a red tile with a white "V") from
both index.html and board.html via a root-absolute href, so it resolves from
"/" and from "/board/" under Vercel cleanUrls. With an icon link present,
browsers stop auto-requesting (and 404ing on) /favicon.ico. No binary asset and
no external dependency, consistent with the self-contained client.

e2e asserts both pages declare the favicon and that /favicon.svg is served as
valid SVG. Closes the last code-finishable item in the roadmap's smaller list.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
thePR0M3TH3ANandClaude Opus 4.8 committed 2026-06-10 19:19:06 -04:00
1 parent 2f423ceed5
commit 23badb03ae
6 files changed
+22 -1

No files matched your search

+1
View File
@@ -28,6 +28,7 @@ site/
locales.js
board.html (bulletin board page)
board.js
favicon.svg (inline SVG site icon)
CNAME
vendor/
nostr-tools/
+2 -1
View File
@@ -240,7 +240,8 @@ should follow the board's trust model.
varies with font availability and zoom; the
[cheat sheet](flyer-field-limits.md) is explicitly approximate. Fine as-is;
noted so it is not mistaken for a contract.
- **No favicon** — browsers log a harmless `404 /favicon.ico`. Cosmetic.
- **Favicon — ✅ done.** A small inline SVG (`site/favicon.svg`) is linked from
both pages, so browsers no longer request (and 404 on) `/favicon.ico`.
- **WebKit/Safari coverage in CI** needs `libavif16` on Linux runners
(`sudo npx playwright install-deps`); headless WebKit is also not a perfect
stand-in for real Safari/iOS print + clipboard behavior, so a real-device
+13
View File
@@ -104,6 +104,19 @@ test.describe("VoxVera static client", () => {
expect(errors, errors.join("\n")).toHaveLength(0);
});
test("both pages declare an SVG favicon (no /favicon.ico 404)", async ({ page }) => {
for (const path of ["/", "/board.html"]) {
await page.goto(path);
const href = await page.locator('link[rel~="icon"]').first().getAttribute("href");
expect(href, `${path} should link a favicon`).toBe("/favicon.svg");
}
// The asset itself is served and is valid SVG.
const res = await page.request.get("/favicon.svg");
expect(res.status()).toBe(200);
expect(res.headers()["content-type"] || "").toContain("svg");
expect(await res.text()).toContain("<svg");
});
test("print media hides app chrome and keeps the flyer", async ({ page }, testInfo) => {
await page.goto("/");
await page.emulateMedia({ media: "print" });
+1
View File
@@ -7,6 +7,7 @@
<!-- Root-absolute asset paths: with Vercel cleanUrls + trailingSlash this
page is served at /board/, so relative paths would resolve to
/board/... and 404. -->
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<link rel="stylesheet" href="/nostr-client.css">
</head>
<body class="board-page">
+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" role="img" aria-label="VoxVera">
<rect width="32" height="32" rx="6" fill="#b80000"/>
<path d="M9 9 L16 23 L23 9" fill="none" stroke="#ffffff" stroke-width="3.6" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 282 B

+1
View File
@@ -4,6 +4,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>VoxVera Nostr Client</title>
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<link rel="stylesheet" href="nostr-client.css">
</head>
<body>