Serve lossless WOFF2 browser fonts outside blocking stylesheets

This commit is contained in:
syntaxbullet
2026-09-08 16:25:37 +02:00
parent 9f552bf608
commit 03eb8d9e7a
14 changed files with 78 additions and 7 deletions

View File

@@ -12,7 +12,7 @@ const result = await Bun.build({
minify: true, minify: true,
splitting: true, splitting: true,
target: "bun", target: "bun",
external: ["@napi-rs/canvas"], external: ["@napi-rs/canvas", "/fonts/browser/*"],
sourcemap: "linked", sourcemap: "linked",
define: { define: {
"process.env.NODE_ENV": JSON.stringify("production"), "process.env.NODE_ENV": JSON.stringify("production"),
@@ -31,3 +31,8 @@ await mkdir(path.join(outdir, "fonts"), { recursive: true });
for (const file of ["InstrumentSerif-Regular.ttf", "NotoSans-Regular.ttf", "NotoSans-Medium.ttf", "NotoSans-SemiBold.ttf"]) { for (const file of ["InstrumentSerif-Regular.ttf", "NotoSans-Regular.ttf", "NotoSans-Medium.ttf", "NotoSans-SemiBold.ttf"]) {
await copyFile(`src/assets/fonts/${file}`, path.join(outdir, "fonts", file)); await copyFile(`src/assets/fonts/${file}`, path.join(outdir, "fonts", file));
} }
await mkdir(path.join(outdir, "fonts/browser"), { recursive: true });
for (const file of ["InstrumentSerif-Regular", "InstrumentSerif-Italic", "NotoSans-Regular", "NotoSans-Medium", "NotoSans-SemiBold"]) {
await copyFile(`src/assets/fonts/${file}.woff2`, path.join(outdir, `fonts/browser/${file}.woff2`));
}

View File

@@ -3,5 +3,5 @@
env = false env = false
[serve.static] [serve.static]
plugins = ["bun-plugin-tailwind"] plugins = ["bun-plugin-tailwind", "./scripts/browser-fonts-plugin.ts"]
env = "BUN_PUBLIC_*" env = "BUN_PUBLIC_*"

View File

@@ -0,0 +1,9 @@
import type { BunPlugin } from "bun";
/** Match the production bundler's external font URLs in the HTML dev server. */
export default {
name: "external-browser-fonts",
setup(build) {
build.onResolve({ filter: /^\/fonts\/browser\// }, args => ({ path: args.path, external: true }));
},
} satisfies BunPlugin;

View File

@@ -1,3 +1,4 @@
import { browserFont } from "../src/browser-fonts";
// Isolated browser QA server. Never opens or changes the user's database. // Isolated browser QA server. Never opens or changes the user's database.
import { sendDueReminders } from "../src/reminders/worker"; import { sendDueReminders } from "../src/reminders/worker";
import type { DiscordFetch } from "../src/sharing/routes"; import type { DiscordFetch } from "../src/sharing/routes";
@@ -55,6 +56,7 @@ const server = Bun.serve({
hostname: "127.0.0.1", hostname: "127.0.0.1",
port, port,
routes: { routes: {
"/fonts/browser/*": browserFont,
"/__preview/reminder": () => new Response(reminderMessage ?? "No reminder sent"), "/__preview/reminder": () => new Response(reminderMessage ?? "No reminder sent"),
"/__preview/shared-image": () => sharingPreview && sharedImage ? new Response(sharedImage) : new Response(null, { status: 404 }), "/__preview/shared-image": () => sharingPreview && sharedImage ? new Response(sharedImage) : new Response(null, { status: 404 }),
"/__preview/login": () => "/__preview/login": () =>

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -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 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 `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. 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
View 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
View 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 });
}

View File

@@ -1,3 +1,4 @@
import { browserFont } from "./browser-fonts";
import { startReminders } from "./reminders/worker"; import { startReminders } from "./reminders/worker";
import { backupConfig, startBackups } from "./ops/backups"; import { backupConfig, startBackups } from "./ops/backups";
import { databasePath } from "./db/config"; import { databasePath } from "./db/config";
@@ -28,6 +29,7 @@ const server = Bun.serve({
hostname: process.env.HOST ?? "127.0.0.1", hostname: process.env.HOST ?? "127.0.0.1",
port: Number(process.env.PORT ?? 3000), port: Number(process.env.PORT ?? 3000),
routes: { routes: {
"/fonts/browser/*": browserFont,
"/api": req => app.fetch(req), "/api": req => app.fetch(req),
"/api/*": req => app.fetch(req), "/api/*": req => app.fetch(req),
"/*": index, "/*": index,

View File

@@ -1,34 +1,34 @@
@font-face { @font-face {
font-family: "Mina Sans"; font-family: "Mina Sans";
src: url("../src/assets/fonts/NotoSans-Regular.ttf") format("truetype"); src: url("/fonts/browser/NotoSans-Regular.woff2") format("woff2");
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
font-display: swap; font-display: swap;
} }
@font-face { @font-face {
font-family: "Mina Sans"; font-family: "Mina Sans";
src: url("../src/assets/fonts/NotoSans-Medium.ttf") format("truetype"); src: url("/fonts/browser/NotoSans-Medium.woff2") format("woff2");
font-style: normal; font-style: normal;
font-weight: 500; font-weight: 500;
font-display: swap; font-display: swap;
} }
@font-face { @font-face {
font-family: "Mina Sans"; font-family: "Mina Sans";
src: url("../src/assets/fonts/NotoSans-SemiBold.ttf") format("truetype"); src: url("/fonts/browser/NotoSans-SemiBold.woff2") format("woff2");
font-style: normal; font-style: normal;
font-weight: 600; font-weight: 600;
font-display: swap; font-display: swap;
} }
@font-face { @font-face {
font-family: "Instrument Serif"; font-family: "Instrument Serif";
src: url("../src/assets/fonts/InstrumentSerif-Regular.ttf") format("truetype"); src: url("/fonts/browser/InstrumentSerif-Regular.woff2") format("woff2");
font-weight: 400; font-weight: 400;
font-style: normal; font-style: normal;
font-display: swap; font-display: swap;
} }
@font-face { @font-face {
font-family: "Instrument Serif"; font-family: "Instrument Serif";
src: url("../src/assets/fonts/InstrumentSerif-Italic.ttf") format("truetype"); src: url("/fonts/browser/InstrumentSerif-Italic.woff2") format("woff2");
font-weight: 400; font-weight: 400;
font-style: italic; font-style: italic;
font-display: swap; font-display: swap;