Skip to content

Switch font-sans from Geist to self-hosted Inter - #13

Open
adamsoffer wants to merge 1 commit into
mainfrom
claude/geist-to-inter-font-1b175d
Open

Switch font-sans from Geist to self-hosted Inter#13
adamsoffer wants to merge 1 commit into
mainfrom
claude/geist-to-inter-font-1b175d

Conversation

@adamsoffer

Copy link
Copy Markdown
Collaborator

Replaces Geist Sans with Inter for font-sans. Geist Mono is unchanged and still backs font-mono. Inter comes from the typeface author's own distribution (rsms.me/inter, OFL), vendored into public/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 (opsz 14–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

  • The font preloads over the connection that already delivered the HTML, instead of waiting on a third-party stylesheet round-trip. That round-trip can't be collapsed with preload against the CDN, because its font URL is version-stamped (?v=4.1) — hardcoding it means a silent double-download on every Inter release.
  • Browser HTTP caches have been partitioned per top-level site since ~2020 (Chrome 86, Firefox 85, Safari earlier), so a shared CDN buys no cross-site cache reuse. Every first-time visitor pays full freight either way.
  • Removes a render-blocking third-party SPOF, and stops leaking visitor IPs to a third party.

What's served

File Size
InterVariable-latin.woff2 107 KB latin + UI symbols — preloaded, the only sans face most sessions fetch
InterVariable.woff2 344 KB pristine distribution file, demand-loaded for non-Latin only
InterVariable-Italic-latin.woff2 118 KB never fetched today; there so a future <em> gets real italics
OFL.txt license, since vendoring redistributes

The part reviewers should actually look at

Every unicode-range in app/fonts.css is 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-10FFFF on 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.py fetches, subsets, generates app/fonts.css, and verifies. Its --check mode 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:

  • fontTools is not in package.json, per the repo's no-new-dependencies rule. It's a hand-run tool when bumping Inter; output is committed. Regenerating needs pip install 'fonttools[woff]'.
  • The check isn't wired into 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-mono overrides 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

  • Normal page load fetches only the 107 KB subset; the 344 KB fallback is never requested
  • Injecting Привет Ελλάδα Tiếng Việt loads the fallback on demand and renders correctly
  • pnpm lint, pnpm typecheck, pnpm build, prettier --check all pass

Repo 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

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>
@vercel

vercel Bot commented Aug 24, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
console Ready Ready Preview Aug 24, 2026 7:13pm

Request Review

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant