Arabic in next/og: which fonts work, and why yours probably doesn’t

We generate an Open Graph card for every page on this site, in English, Spanish and Arabic, using next/og. The Latin ones worked immediately. The Arabic ones failed in three different ways before they worked, and none of the failures were obvious from the error.

next/og renders through satori, which is not a browser. What follows is what we measured.

1. Most Arabic fonts crash it outright

The error is lookupType: 5 - substFormat: 3 is not yet supported. That lookup is the contextual substitution table — the one that turns isolated letters into their initial, medial and final forms. It is how Arabic joins. Satori cannot read that format, so a font that uses it does not render at all.

We swept every candidate over all 90 Arabic strings on the site rather than one sample, which matters: a single test string gave a false pass on a font that fails on 104 of them.

FontResult
Noto Naskh ArabicFails — every string
AmiriFails — every string
Scheherazade NewFails — every string
Noto Sans ArabicFails — every string
CairoFails on 1 of 104
IBM Plex Sans ArabicPasses
AlmaraiPasses
TajawalPasses
Markazi TextPasses

The Naskh faces — the ones you would actually want for scripture — are the ones that fail. We set Arabic in IBM Plex Sans Arabic on the cards for that reason, and it is defensible here because a card title is chrome rather than scripture.

2. Font order decides shaping, not glyph coverage

This is the one that wastes an afternoon. Satori shapes text with the first font in the array, regardless of whether that font has the glyphs.

We registered a Latin display face and an Arabic face under one family name, which is normal CSS practice. Arabic then went through the Latin font's tables and died on the same substFormat: 3 error — making it look like the Arabic font was at fault when it was never consulted. Measured: Latin first fails all 90 strings, Arabic first passes all 90.

The fix is to register one font per card and choose it by the card's language. One font is simpler than a load-bearing array order.

3. Satori does not reorder right-to-left text

With a working font, the letters join correctly and the words come out in the wrong order — the first word of the sentence on the left, which is the sentence backwards.

direction: rtl does not fix it. We tried it on the container, on the text node, and on both. Satori shapes the glyphs and lays the runs out left to right regardless.

So reverse the words yourself and let satori's left-to-right layout put them right. The catch is that this only holds for a single line: if satori wraps a string you have already reversed, the last words end up on the top line and the result is gibberish. Break the lines first, reverse each one on its own, render each as its own element, and set white-space: nowrap so the line breaks are decided once rather than twice.

Line widths have to be estimated in characters, because satori exposes no metrics at build time. Calibrate against your longest and shortest real strings rather than guessing, and when an estimate would overflow, prefer dropping the text to cutting it mid-clause.

The short version

  • Pick a font satori can actually read — test it against your real corpus, not one string.
  • Register one font per image, chosen by language. Never two.
  • Break and reverse RTL lines yourself, then nowrap them.

None of this is in the documentation, and all three failures look like a font problem when only the first one is.

Notes from the workshop.