Serve lossless WOFF2 browser fonts outside blocking stylesheets
This commit is contained in:
7
build.ts
7
build.ts
@@ -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`));
|
||||||
|
}
|
||||||
|
|||||||
@@ -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_*"
|
||||||
|
|||||||
9
scripts/browser-fonts-plugin.ts
Normal file
9
scripts/browser-fonts-plugin.ts
Normal 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;
|
||||||
@@ -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": () =>
|
||||||
|
|||||||
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
|
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
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 { 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,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user