Skip to content

Build1 publisher3 min readPublished

Satori's missing WOFF2 support puts four Geist .ttf files in a Next.js repo

Satori, the renderer behind next/og, cannot read WOFF2, so one Next.js site commits four Geist .ttf files to set its preview cards in the site's own type. The card takes its logo and palette from source, but its font is a second copy that someone has to keep current.

The Engineer · Build desk

Illustration accompanying Satori's missing WOFF2 support puts four Geist .ttf files in a Next.js repo

What happened

  • An opengraph-image.tsx in the App Router's root segment gives every route without its own card an og:image, so pages added later get a card without anyone asking for one.
  • Satori supports only a small CSS subset (flexbox, absolute positioning, gradients and text), with no grid, floats or calc.
  • A single line, export const dynamic = "force-static", prerenders the card into a plain PNG served from the CDN.
  • The font paths are four literal readFile strings because Turbopack traces the whole project into the bundle when it cannot evaluate a path built from variables.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

  • cost Geist now has two sources, the WOFF2 the page fetches and four committed .ttf files, and keeping them matched is up to whoever maintains the repo.
  • constraint Vendoring depends on the font licence. Geist's OFL permits committing the files, and a brand font without that permission leaves a team with Satori's fallback type.
  • decision Prerendered cards change only when the site rebuilds. That suits cards drawn from files in the repo and rules out cards built from per-request data.

The card is a React component. It reads the logo off disk, uses the same colour names as the stylesheet and takes its words from the registry the pages are built from [15]. The author, writing on dev.to, built it to replace the usual PNG exported once into public/. "That PNG is wrong within a month," the author wrote [14].

The site loads Geist through next/font/google, which fetches WOFF2 [2]. Satori reads TrueType and OpenType. It does not read WOFF2 [1]. So the font the app already has cannot go into the card. The four faces the card uses (regular, semibold, bold and mono) are committed to assets/fonts/ as .ttf files and read off disk at render time [3]. According to the author, Geist's OFL licence permits this [4]. The author was blunt about the other option: "The alternative is a card set in whatever Satori falls back to, which is a card that looks like somebody else's product." [18]

The logo gets stricter treatment. Satori will not rasterise a raw svg child, so public/logo.svg is read as bytes and put into an img tag as a base64 data URI [11]. Redrawing it as Satori-friendly JSX would have created a second logo that drifts from the first [19]. "Two logos is worse than no card," the author wrote [13].

The fonts do not get that protection. The page's Geist comes through next/font/google, and the card's comes from four committed files [2][3]. The post does not describe a check that keeps the two matched. I think vendoring is still the right call for this site, because fallback type is the only other route [18]. The four .ttf files are still a second copy of a brand asset, and refreshing them is a job somebody has to own.

Prerendering the card settles two risks the author raises. The renderer reads five files from the filesystem, one SVG and four fonts. Those files are certain to exist on the build machine but not necessarily inside a serverless bundle at request time [8]. And according to the author, several crawlers and chat clients will not wait for a cold function to draw an image. They show the link as grey text instead [9].

For this setup to carry over, the brand font must exist as TrueType or OpenType under a licence that allows committing it [1][4]. Card layouts need to stay simple. At 1200 by 630, this card is, in the author's words, "six boxes stacked in a column" [16]. Card count matters too. The site has six cluster cards for about seventy public pages, roughly 12 pages per card [12][1]. The author wrote that per-page cards would mean "sixty more strings to keep true" [17].

What to watch

  • Satori adding WOFF2 support would let the card use the font the page already loads and retire the four committed .ttf files.
  • A Geist release that changes glyphs or metrics would be the first real test of whether the committed .ttf files and the next/font/google copy drift apart.
  • A move from six cluster cards to per-page cards would add about sixty strings that have to stay accurate.
Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories