Why satori throws "ltagTable is not defined" on a woff font

satori throws ltagTable is not defined when a font contains an ltag table. Its font parser, a 2022 fork of opentype.js, assigns that variable without declaring it, and strict mode turns the assignment into a ReferenceError. The woff format is not the cause. I shipped a vendored OTF; stripping the table or patching one line also works.

The build error satori gives for one font

This site renders an Open Graph card for every page at build time, with satori and resvg; the current build writes 62 of them. Each card uses three typefaces. Newsreader and Instrument Sans load from fontsource woff files and parse fine. Commit Mono, the face for the umar.codes line at the bottom of the card, stopped the first build of the renderer on 2026-07-27:

ReferenceError: ltagTable is not defined
    at Object.parseBuffer [as parse] (…/@shuding/opentype.js/dist/opentype.js:11505:30)
    at Wt.addFonts (…/satori/dist/index.js:6:50501)

The message names a variable, not a font, so the obvious guess is the file format. That was my guess in July. The comment I wrote in og.ts that day still says the fontsource woff “trips an opentype.js bug”, which is true but points at the wrong thing.

The woff format is not the cause

I reproduced it today against satori 0.29.0 under Node 24.13.0: one line of 26-pixel text, one font per run.

Font file Signature Bytes Result
Commit Mono 400 woff wOFF 57,580 ReferenceError: ltagTable is not defined
Commit Mono 400 woff2 wOF2 48,128 Error: Unsupported OpenType signature wOF2
Commit Mono 400 OTF, upstream OTTO 161,124 renders in 17 ms
Newsreader 600 woff wOFF 29,784 renders in 4 ms
Instrument Sans 600 woff wOFF 21,676 renders in 3 ms

Two woff files render, so the container is fine. The woff2 failure is a different limit: satori’s parser does not read woff2 at all, and the error says so. The real difference is inside the file. The Commit Mono woff lists 11 tables, and one of them is ltag. Neither of the other woffs has one, and the upstream OTF has 13 tables and no ltag. All 12 Commit Mono .woff files in @fontsource/commit-mono 5.3.0 carry it.

ltag is an Apple table: a list of language tags that other tables refer to by index. Nothing on an OG card needs it.

One missing declaration in satori’s pinned parser

satori does not use opentype.js from npm. It pins @shuding/opentype.js 1.4.0-beta.0, a fork last published on 2022-08-26. In upstream opentype.js 1.3.4, parseBuffer declares var ltagTable; before it loops over the font’s tables. The fork’s parseBuffer lost that line, but its case 'ltag': branch still assigns ltagTable = ltag.parse(…). In strict-mode code, an assignment to an undeclared name throws. So the bug does nothing for every font without the table, and stops the build for every font with it.

I checked that this one line is the whole bug. In a scratch copy of the fork with var ltagTable; added, the Commit Mono woff parses to 1,932 glyphs, the same count as the OTF. Upgrading satori does not help: the latest release on npm, 0.63.2, still pins the same parser version. A search of satori’s issue tracker for the error text returned no results on 2026-10-21.

Three fixes for a font with an ltag table

I would make the same call today, for a different reason. In July the OTF was the first thing that worked. Now the argument is scope: one file, one consumer, and a failure that stops the build instead of rendering a wrong card. Its real cost is drift. The OG card and the web page draw Commit Mono from two files that can change separately, and no check compares them. For one 26-pixel line, I accept that.

What the OG renderer costs per card

With the vendored OTF in place, renderOgImage took 1,297 ms for the first card in a fresh process and a median of 93 ms per card after that, including the PNG rasterisation in resvg. At 62 cards, that is about seven seconds of a full build. Loading the module and reading the three fonts took 79 ms.

List the tables before you blame the format

When a font parser throws a ReferenceError, the error is about the parser’s code, so the useful question is which code path the font took. A woff’s table directory is 20 bytes per table at a fixed offset, and a dozen lines of Node print it. The answer here was one of 11 four-letter tags. It took me three months and a scheduled post to read them.

The renderer is part of the Astro build of this site; it runs once per page and never in the browser.

Revisions

  1. Created from this site's OG renderer (src/lib/og.ts), the vendored Commit Mono OTF, the 2026-07-27 decision entry, and a reproduction run that day against satori 0.29.0 and @shuding/opentype.js 1.4.0-beta.0 under Node 24.13.0.
  2. Published on its Oct 21 slot; auto-drafted by the publishing run because no draft existed for the row.