Why I built FontFridge
Google Fonts offers thousands of families, but its built-in search covers only a small part of what each font can do. Details such as language coverage, stylistic features, and variable settings are stored in the font files, where they are hard to compare.
I wanted people to search those details directly, instead of opening family pages one by one and guessing.
Each catalog entry is built from font metadata, so visitors can compare families by their actual design properties.
Reading the font files
FontFridge reads information from 1,942 families and makes it searchable across 66 details. People can look for practical things such as language coverage, available styles, variable axes, or whether a family includes a slashed zero. Across the catalog, that includes 273 OpenType feature tags, 49 variable axes, 980 languages, and 45 writing systems. A designer can also filter by numeric measurements such as x-height or by the foundry’s vendor ID, then combine those constraints rather than checking each property on a separate specimen page.
The source of truth matters. Rather than relying only on labels entered by a person, the data comes from the font files and Google’s published font data. The Python harvester reads tables such as OS/2, fvar, GSUB, and GPOS with fontTools to collect measurements, variable axes, and OpenType features. Other details, including license text, designer profiles, and repository activity, come from Google’s APIs and upstream project metadata. A set of enrichment steps brings those sources together for each family.
The pipeline extracts font metadata, checks coverage, and publishes a prepared dataset to R2.
The catalog is read-only and the same for every visitor. I prepared the data ahead of time and publish it as static files, instead of running a database and a query server. The daily job publishes a content-hashed dataset to R2 and updates a small manifest. The site build verifies the downloaded data before producing the files the Worker serves. Since the catalog itself is generated, it does not need to live in Git or create a commit when only upstream font metadata changes.
The complete dataset is about 20 MB, so sending it all on the first page load would make browsing slow. I split the data by how each page reads it, rather than making every route download the same record shape. The first 24 families can render on the server for a useful initial page; the browser then loads the full catalog once for instant filtering. Detail pages read a small file for one family, and programmatic search uses a slimmer projection. Glyph coverage has its own deduplicated index so checking a sample of text does not require downloading every family file.
The detail route also needed to work during server rendering. A Worker cannot fetch its own public URL to retrieve a static file without sending the request back through itself, so the server uses the platform’s asset binding while the browser uses a normal fetch. That keeps the route isomorphic and gives crawlers real page titles and descriptions without forcing the full catalog into server memory.
The catalog is also available to AI tools through a search endpoint. An assistant can ask for a font with specific language support or features and get matching families without loading the entire dataset into its own context. The MCP service exposes search, family detail, and category listing tools. It also serves an OpenAPI description and an agent skill, while pre-sharded facets let an agent narrow the set using a few dozen kilobytes at a time. The browser and an assistant share the same facts, but each receives the data shape that fits its way of searching.
A daily incremental update is quick, but it can miss a family if an upstream source changes without updating its timestamp. I paired it with a monthly full harvest that rechecks every family, plus a guard that stops a publish if the dataset unexpectedly shrinks by more than 20%. That catches partial harvests before they can replace a healthy catalog. Both paths use the same publish flow: upload a content-hashed snapshot to R2, update a small manifest, and trigger a build only if the dataset checksum changed. Old snapshots expire after 30 days, leaving a month of rollback points without growing the Git history.
The same catalog serves three different readers. Browsers filter the JSON locally, crawlers receive server-rendered detail pages, and AI tools can use an MCP endpoint with search and family-detail operations. I also publish smaller facet files and API descriptions so an agent can narrow candidates without loading the whole catalog into its context. These are separate read paths over one prepared source, rather than separate copies of the underlying font data.
The Worker also negotiates between HTML and Markdown for font detail requests. Browsers and social crawlers receive rendered pages with the right title and metadata, while an agent that asks for Markdown can receive a compact text representation. Filtered browse URLs are kept out of the search index, so crawlers can discover each font page without generating a huge set of near-duplicate search results.
Each font also gets an Open Graph card rendered in that family’s own typeface. The build traces the glyph paths for the family name into the preview image, so the share card demonstrates the face it is linking to. Generating those cards at build time keeps a request for a social preview from having to render typography dynamically at the edge.
Caching took its own design work. Font detail pages are identical for every visitor, so caching their rendered HTML avoids repeating expensive server rendering. But a cache can outlive a deployment while the content-hashed assets from that build do not. I tied the home-page cache to the code build and the detail-page cache to the dataset version, so a code-only deploy does not invalidate every font page, while a new dataset refreshes the relevant pages. Only successful responses enter the cache. A short cold-cache period after deployment is safer than serving HTML that points to deleted assets.
The two harvest schedules differ only in how they identify changed families. The daily job trusts upstream timestamps and handles additions, updates, and removals; the monthly job ignores that diff and reconciles the complete catalog. Removed families are marked unpublished rather than deleted, so a temporary disappearance upstream is reversible. Both jobs refresh metadata that changes independently of the font binaries, then checksum the assembled dataset to decide whether it is worth publishing. Their shared concurrency lock also prevents a manual full run from racing a scheduled update to the manifest. For a contributor without access to the private R2 bucket, the build falls back to a small committed sample, so cloning the repository still provides a working development site.
Not every field has the same meaning of “updated.” A font designer may have stopped maintaining a project, Google may have recompiled a binary, or the API may have refreshed its record on a different date. I kept these timestamps separate rather than collapsing them into one label. That gives the detail page a more honest history and avoids suggesting that every upstream update means the typeface itself changed.
One catalog serves rendered pages to visitors and crawlers, plus a compact Markdown response for agents.
Daily scans process changed fonts; a monthly scan rebuilds the catalog, and both update the same manifest.
What changed
FontFridge turns font information that was buried inside files into something people can explore and compare. A search that once meant opening many tabs can now start with the exact feature or language someone needs.
The project also changed how I think about databases. When information is read-only and refreshes on a schedule, preparing it ahead of time can be simpler than building a live query system.