Serve lossless WOFF2 browser fonts outside blocking stylesheets
This commit is contained in:
BIN
src/assets/fonts/InstrumentSerif-Italic.woff2
Normal file
BIN
src/assets/fonts/InstrumentSerif-Italic.woff2
Normal file
Binary file not shown.
BIN
src/assets/fonts/InstrumentSerif-Regular.woff2
Normal file
BIN
src/assets/fonts/InstrumentSerif-Regular.woff2
Normal file
Binary file not shown.
BIN
src/assets/fonts/NotoSans-Medium.woff2
Normal file
BIN
src/assets/fonts/NotoSans-Medium.woff2
Normal file
Binary file not shown.
BIN
src/assets/fonts/NotoSans-Regular.woff2
Normal file
BIN
src/assets/fonts/NotoSans-Regular.woff2
Normal file
Binary file not shown.
BIN
src/assets/fonts/NotoSans-SemiBold.woff2
Normal file
BIN
src/assets/fonts/NotoSans-SemiBold.woff2
Normal file
Binary file not shown.
@@ -12,3 +12,24 @@ Downloaded September 7, 2026. The files are versioned in this repository and cop
|
||||
into the production image. Browser and native canvas renderers use the alias
|
||||
`Mina Sans`; no system sans-serif font is selected for card text. The native font
|
||||
loader fails startup if a required font cannot be registered.
|
||||
|
||||
Browser WOFF2 copies are lossless conversions of these exact TTF files using
|
||||
FontTools with Brotli; no subsetting is applied. All glyphs and Unicode mappings
|
||||
are retained (3,748 glyphs for each Noto Sans weight, 377 for each Instrument
|
||||
Serif style). Native canvas keeps using the original TTFs. CSS uses separately
|
||||
cacheable `/fonts/browser/*.woff2` URLs so font bytes do not block CSS parsing.
|
||||
|
||||
Regenerate with `uv run --with fonttools --with brotli python`:
|
||||
|
||||
```python
|
||||
from pathlib import Path
|
||||
from fontTools.ttLib import TTFont
|
||||
for path in Path("src/assets/fonts").glob("*.ttf"):
|
||||
font = TTFont(path)
|
||||
cmap, glyphs = font.getBestCmap(), font.getGlyphOrder()
|
||||
font.flavor = "woff2"
|
||||
font.save(path.with_suffix(".woff2"))
|
||||
restored = TTFont(path.with_suffix(".woff2"))
|
||||
assert restored.getBestCmap() == cmap
|
||||
assert restored.getGlyphOrder() == glyphs
|
||||
```
|
||||
|
||||
14
src/browser-fonts.test.ts
Normal file
14
src/browser-fonts.test.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { browserFont } from "./browser-fonts";
|
||||
test("browser fonts are external WOFF2 files with conditional caching and a fixed allowlist", async () => {
|
||||
const url = "http://localhost/fonts/browser/InstrumentSerif-Regular.woff2";
|
||||
const response = browserFont(new Request(url));
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.headers.get("Content-Type")).toBe("font/woff2");
|
||||
const bytes = new Uint8Array(await response.arrayBuffer());
|
||||
expect(new TextDecoder().decode(bytes.slice(0, 4))).toBe("wOF2");
|
||||
expect(browserFont(new Request(url, { headers: { "If-None-Match": response.headers.get("ETag")! } })).status).toBe(304);
|
||||
expect(browserFont(new Request("http://localhost/fonts/browser/.env")).status).toBe(404);
|
||||
expect(browserFont(new Request("http://localhost/fonts/browser/%2e%2e%2f.env")).status).toBe(404);
|
||||
expect(await browserFont(new Request(url, { method: "HEAD" })).text()).toBe("");
|
||||
});
|
||||
18
src/browser-fonts.ts
Normal file
18
src/browser-fonts.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import path from "node:path";
|
||||
|
||||
const names = ["InstrumentSerif-Regular", "InstrumentSerif-Italic", "NotoSans-Regular", "NotoSans-Medium", "NotoSans-SemiBold"];
|
||||
const directory = process.env.NODE_ENV === "production" ? "fonts/browser" : "src/assets/fonts";
|
||||
const fonts: Map<string, { file: ReturnType<typeof Bun.file>; etag: string }> = new Map(names.map(name => {
|
||||
const file = Bun.file(path.resolve(directory, `${name}.woff2`));
|
||||
return [`${name}.woff2`, { file, etag: `"${file.size.toString(16)}-${file.lastModified.toString(16)}"` }] as const;
|
||||
}));
|
||||
|
||||
/** Explicit allowlist: font URLs never grant access to other filesystem paths. */
|
||||
export function browserFont(request: Request): Response {
|
||||
const name = new URL(request.url).pathname.slice("/fonts/browser/".length);
|
||||
const font = fonts.get(name);
|
||||
if (!font || !["GET", "HEAD"].includes(request.method)) return new Response(null, { status: 404 });
|
||||
const headers = { "Content-Type": "font/woff2", "Cache-Control": "public, max-age=3600", ETag: font.etag };
|
||||
if (request.headers.get("If-None-Match") === font.etag) return new Response(null, { status: 304, headers });
|
||||
return new Response(request.method === "HEAD" ? null : font.file, { headers });
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { browserFont } from "./browser-fonts";
|
||||
import { startReminders } from "./reminders/worker";
|
||||
import { backupConfig, startBackups } from "./ops/backups";
|
||||
import { databasePath } from "./db/config";
|
||||
@@ -28,6 +29,7 @@ const server = Bun.serve({
|
||||
hostname: process.env.HOST ?? "127.0.0.1",
|
||||
port: Number(process.env.PORT ?? 3000),
|
||||
routes: {
|
||||
"/fonts/browser/*": browserFont,
|
||||
"/api": req => app.fetch(req),
|
||||
"/api/*": req => app.fetch(req),
|
||||
"/*": index,
|
||||
|
||||
Reference in New Issue
Block a user