fontfetch
A foundry-style inspector for the public web's typography — paste a URL, get every webfont, classified by licence, in seconds.
Summary
fontfetch is a TypeScript monorepo split across an open-source CLI (`fontfetch` on npm) and a private fullstack webapp (fontfetch.dev). The pipeline walks every stylesheet a browser would, downloads each `@font-face` source, classifies it as open / commercial / unknown via URL-signature + family-name heuristics, and emits a project-ready bundle with framework-specific configs, zero-CLS fallback metrics (capsize), terminal-native font inspection (fontkit), and render-aware subsetting (harfbuzzjs WASM, no Python). The webapp wraps the pipeline in a streaming SSE UI with foundry-style font previews, side-by-side compare, and a community pairings registry.
Target user
Web designers, developers, and brand teams who need to self-host, audit, or reverse-engineer the typography of any live site — without the GDPR liability of hot-linking Google Fonts or the legal ambiguity of grabbing commercial faces.
The public web is the world’s largest type specimen. Extracting anything useful from it is surprisingly painful.
If you want to self-host the fonts a site uses — to satisfy GDPR (the 2022 LG München ruling makes Google Fonts hot-linking a €100-per-IP liability in the EU), to ship faster, or just to audit a competitor’s typography — you have to read @font-face rules across stylesheets that import other stylesheets, follow each src: url(…)to its CDN, figure out whether the foundry’s licence permits redistribution, and stitch the result into something your build system can consume.
Existing tooling is fragmented: webfont-dl covers the download step, Wakamai Fondue covers inspection but only in a browser, fontaine covers fallback metrics but only inside Nuxt, glyphhanger covers subsetting but pulls in a Python fonttools dependency. No single tool covers the full extract → inspect → subset → ship flow.
One command. Every webfont. Licence-classified.
npx fontfetch <url> produces a folder with every webfont, an emitted fonts.css (with font-display: swap and <link rel=preload> hints), framework configs for Next.js / Tailwind / vanilla CSS, and a per-face LICENSE_REVIEW.md. With the right flag it also emits capsize-driven zero-CLS fallback blocks and harfbuzzjs-subset variants containing only the codepoints the page renders.
- 01CLIExtract & emit
Walk @import-chained stylesheets, download every face, write a provenance-bucketed bundle.
- 02CLIInspect
Terminal-native Wakamai-Fondue equivalent. Glyph count, axes, OpenType features, OFL detection.
- 03CLISubset & fallback
harfbuzzjs WASM subsetting, capsize size-adjust fallbacks. Pure Node, no Python dep.
- 04WebappFoundry-style UI
Paste URL, watch the SSE pipeline run, then open each family as a type specimen.
Open-core architecture. The pipeline lives in @fontfetch/core — a workspace package consumed by the published CLI (via tsup --noExternal) and by the private webapp (via workspace:*). The SSE wire format is literally core’s PullProgressEvent discriminated union. No parallel codepaths between surfaces.
Open-core in two pieces. A public OSS surface that consumers see on npm, and a private fullstack monorepo that the webapp ships from. One-way sync (public → private) preserves SHAs across the boundary, so git blame works cleanly across the open / closed split.
fonts/opensource
publicGitHub · public
- packages/clinpm: fontfetch
- packages/coreopen-source pipeline
- pairings/community registry
- docs/public
fonts/fullstack
privateGitHub · private
- apps/webNext.js 16 webapp
- apps/workerplaceholder
- apps/apiplanned
- docs/webapp-planprivate
The user-facing flow streams events from server to client over SSE while a Node-runtime route handler walks the URL’s stylesheets, downloads every @font-face binary, and emits typed progress frames the client renders in real time.
Client
publicBrowser · React 19
- validateUrl()guard + paste
- POST /api/pullfetch + ReadableStream
- FontPreviewfoundry-style UI
Server
privateNext.js 16 · Node runtime
- guardUrl()SSRF + DNS check
- pull()crawl, parse, license, emit
- data: <event>SSE frames
- 01
Shipped a Node-native font extraction pipeline that walks `@import`-chained stylesheets, captures every `@font-face` with weight/style/unicode-range/source intact, and writes provenance-bucketed output (`files/<google|adobe-typekit|commercial|open-cdn|self-hosted>/<name>`).
- 02
Built `fontfetch inspect` — terminal-native Wakamai Fondue equivalent that prints glyph count, units-per-em, variation axes, OpenType features, name-table strings, vendor, copyright, and SIL OFL detection with Reserved Font Name (RFN) clause awareness.
- 03
Built `--fallback` flag emitting framework-agnostic capsize-driven `size-adjust` / `ascent-override` / `descent-override` / `line-gap-override` blocks — solves the same CLS problem `fontaine` solves without the Nuxt/Vite coupling.
- 04
Built `fontfetch subset` — Playwright DOM walk + harfbuzzjs WASM subset, pure-Node, no Python `fonttools` dependency (unlike `glyphhanger`).
- 05
Shipped a license classifier with first-match-wins precedence across known commercial CDNs (Adobe Typekit, Monotype fast.fonts.net, Hoefler cloud.typography.com, Type Network) plus a curated SIL OFL / Google Fonts fallback catalog, biased toward false-commercial as the safer failure mode.
- 06
Designed and implemented an SSE Route Handler (`POST /api/pull`, Node runtime, `maxDuration=90`) that streams `PullProgressEvent`s as `data: <json>\n\n` frames over a fetch-reader pattern (POST + body, not EventSource), with `req.signal` abort propagation and `X-Accel-Buffering: no` to defeat ALB buffering.
- 07
Built a three-layer SSRF guard (protocol whitelist → hostname class blocklist → DNS resolution against RFC1918 / link-local / AWS metadata `169.254.169.254`) that runs before any network I/O — public-looking hostnames resolving into private CIDRs are caught.
- 08
Set up FAANG-grade open-core dual-repo workflow: public `fontfetch` repo + private `fullstack` repo as sibling clones, with public→private sync via `git fetch public && git merge public/main` preserving original public SHAs in private history (Vercel/GitLab/Sentry pattern).
- L01
Heuristic classifiers should be biased toward the safe failure mode.
The license classifier prefers false-commercial over false-open because a user shipping a commercial face they thought was OFL has a legal problem; a user told their OFL face is commercial just runs `--force`.
- L02
Framework-agnostic beats framework-native when the underlying capability is identical.
`--fallback` works in any CSS surface (Next.js with or without next/font, Astro, Tailwind, plain HTML) precisely because we resisted the urge to ship a Vite plugin. Each framework integration is a smaller audience; the capability is the wedge.
- L03
When you publish an open-core CLI, your private webapp should consume the same library — not a fork.
`@fontfetch/core` is a workspace package the CLI bundles via `tsup --noExternal` and the webapp imports as `workspace:*`. The wire format for the SSE event stream is literally the library's `PullProgressEvent` type, with two extra SSE-only variants (`accepted`, `fatal`). Zero parallel codepaths.
- cli versions shipped
- 14
- core source loc
- ~2050
- webapp source loc
- ~2500
- framework emitters
- 3