Switch font-sans from Geist to self-hosted Inter - #13
Open
adamsoffer wants to merge 1 commit into
Open
Conversation
Geist Sans is replaced by Inter for `font-sans`; Geist Mono is unchanged and still backs `font-mono`. Inter comes from the typeface author's own distribution (https://rsms.me/inter/, OFL), vendored into public/fonts/ rather than linked from the CDN. Why Inter here: the console subtree runs a 13.5px body with 10.5-11px labels, so every glyph in the UI sits at or below the small end of Inter v4's optical size axis (opsz 14-32) and benefits from it automatically. Geist has one drawing at every size. Inter's more open apertures also hold up better against light-on-dark bloom at those sizes, and its 2,852-codepoint coverage (Greek, Cyrillic, Vietnamese) matters for user-supplied display and org names. Why self-hosted rather than the rsms.me CDN: the font now preloads over the connection that already delivered the HTML instead of waiting on a third-party stylesheet round-trip, which cannot be collapsed with `preload` because the CDN font URL is version-stamped. Browser HTTP caches have been partitioned per top-level site since ~2020, so a shared CDN buys no cross-site cache reuse. Self-hosting also removes a render-blocking third-party SPOF and stops leaking visitor IPs. Two roman faces are served: InterVariable-latin.woff2 107 KB latin + UI symbols, preloaded InterVariable.woff2 344 KB pristine, demand-loaded for non-Latin Every `unicode-range` is generated from the actual cmap of the woff2 it points at, never hand-written. This is load-bearing, not tidiness: a hand-written blanket range made the browser fetch all 344 KB for a single `✕` in the Skip button, discover the glyph was absent from Inter entirely, and only then fall through to a system font — 448 KB total, worse than the CDN baseline it replaced. Deriving the ranges designs that class of bug out and makes the two roman ranges disjoint, so declaration order carries no meaning. scripts/build-inter-fonts.py fetches, subsets, generates app/fonts.css, and verifies. Its `--check` mode fails on any glyph that would land on the 344 KB fallback, and reports glyphs Inter lacks entirely (`✕`, `▾`) separately as zero-cost. fontTools is deliberately not a package.json dependency — the script is run by hand when bumping Inter and its output is committed. The `--font-sans` / `--font-mono` overrides in the (app) and (auth) layouts are removed: they only restated what the root layout already sets, and as inline styles they would have pinned those subtrees to a single face. Verified in-browser: a normal page load fetches only the 107 KB subset and never requests the fallback; injecting Cyrillic/Greek/Vietnamese text loads it on demand and renders correctly. lint, typecheck, build, and prettier pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Replaces Geist Sans with Inter for
font-sans. Geist Mono is unchanged and still backsfont-mono. Inter comes from the typeface author's own distribution (rsms.me/inter, OFL), vendored intopublic/fonts/rather than linked from the CDN.Why Inter for this UI
The console subtree runs a 13.5px body with 10.5–11px labels, so every glyph in this UI sits at or below the small end of Inter v4's optical size axis (
opsz14–32) and picks up the small-size drawing automatically. Geist has one drawing at every size. Inter's more open apertures also survive light-on-dark bloom better at those sizes, and its 2,852-codepoint coverage (Greek, Cyrillic, Vietnamese) matters because the console renders user-supplied display and org names.Worth noting the tradeoff: this leaves a mixed pairing (Inter Sans + Geist Mono). Geist Sans/Mono were drawn together, so all-Geist is more coherent on paper. In practice the mono here is confined to IDs, hashes, and tabular numerals rather than running text, where Geist Mono sits fine next to Inter.
Why self-hosted rather than the CDN
preloadagainst the CDN, because its font URL is version-stamped (?v=4.1) — hardcoding it means a silent double-download on every Inter release.What's served
InterVariable-latin.woff2InterVariable.woff2InterVariable-Italic-latin.woff2<em>gets real italicsOFL.txtThe part reviewers should actually look at
Every
unicode-rangeinapp/fonts.cssis generated from the actual cmap of the woff2 it points at, never hand-written. That's load-bearing, not tidiness.The first cut used a hand-written blanket
unicode-range: U+0100-10FFFFon the fallback face. Measured in the browser, the page pulled 448 KB — worse than the CDN baseline it replaced. The cause was a single✕(U+2715) in the Skip button, and chasing it turned up something non-obvious:✕and▾don't exist in Inter at any weight. They always rendered in a system font. But because the hand-written range claimed them, the browser downloaded all 344 KB, found the glyph missing, and only then fell through — pure waste on every page load.The rule is asymmetric, and not the intuitive one: over-declaring the subset range is harmless (that file downloads anyway), while over-declaring the fallback is what costs 344 KB. Deriving both ranges from the built files designs that class of bug out, and makes the two roman ranges disjoint — which also removed a fragile "declaration order is load-bearing" trick the first version depended on.
Tooling
scripts/build-inter-fonts.pyfetches, subsets, generatesapp/fonts.css, and verifies. Its--checkmode fails on any glyph that would land on the fallback, and reports Inter-absent glyphs separately as zero-cost so the two cases don't get confused. Tested by injecting anΩ— exits 1 and names the file.Two deliberate calls:
package.json, per the repo's no-new-dependencies rule. It's a hand-run tool when bumping Inter; output is committed. Regenerating needspip install 'fonttools[woff]'.pnpm lint, since it needs Python + fontTools that CI may not have. Easy to add if you'd prefer it gated.Also removes the
--font-sans/--font-monooverrides from the(app)and(auth)layouts — they only restated what the root layout already sets, and as inline styles they'd have pinned those subtrees to a single face.Verification
Привет Ελλάδα Tiếng Việtloads the fallback on demand and renders correctlypnpm lint,pnpm typecheck,pnpm build,prettier --checkall passRepo weight: adds 580 KB of binaries, mostly the 344 KB fallback. Unavoidable when vendoring; droppable only by accepting tofu on non-Latin display names.
🤖 Generated with Claude Code