Back to work
In Development2026Maintainer & Full-Stack Engineer62% complete

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.

§ 01Problem

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.

§ 02Solution

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.

  • 01CLI
    Extract & emit

    Walk @import-chained stylesheets, download every face, write a provenance-bucketed bundle.

  • 02CLI
    Inspect

    Terminal-native Wakamai-Fondue equivalent. Glyph count, axes, OpenType features, OFL detection.

  • 03CLI
    Subset & fallback

    harfbuzzjs WASM subsetting, capsize size-adjust fallbacks. Pure Node, no Python dep.

  • 04Webapp
    Foundry-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.

§ 03Architecture

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

public

GitHub · public

  • packages/clinpm: fontfetch
  • packages/coreopen-source pipeline
  • pairings/community registry
  • docs/public

fonts/fullstack

private

GitHub · private

  • apps/webNext.js 16 webapp
  • apps/workerplaceholder
  • apps/apiplanned
  • docs/webapp-planprivate
Open-core repo split — public ↔ private, one-way sync

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

public

Browser · React 19

  • validateUrl()guard + paste
  • POST /api/pullfetch + ReadableStream
  • FontPreviewfoundry-style UI

Server

private

Next.js 16 · Node runtime

  • guardUrl()SSRF + DNS check
  • pull()crawl, parse, license, emit
  • data: <event>SSE frames
Streaming pipeline — POST /api/pull, ReadableStream, abort-safe
§ 04Stack
01Primary
TypeScriptNode 20+Next.jsReact
02Infrastructure
pnpm 9 workspacestsupvitestAWS ECS FargateALB + ACMS3Upstash Redis
03Integrations
Playwrightharfbuzzjs via subset-fontfontkit@capsizecss/core + @capsizecss/unpack
04UI / Frontend
Tailwind CSSshadcn/uimotion.devZustandnext-themeslucide-reactsonnerGeist + Geist Mono via next/font
§ 05Key features
  1. 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>`).

  2. 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.

  3. 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.

  4. 04

    Built `fontfetch subset` — Playwright DOM walk + harfbuzzjs WASM subset, pure-Node, no Python `fonttools` dependency (unlike `glyphhanger`).

  5. 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.

  6. 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.

  7. 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.

  8. 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).

§ 06Hardest problems
§ 07What I learned
  • 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.

§ 08By the numbers
vitest cases passing
101/101
cli versions shipped
14
core source loc
~2050
webapp source loc
~2500
framework emitters
3