#802·satori

0.33.0+ HarfBuzz: WOFF glyphs render as stacked horizontal bars (Inter Bold)

Author: NooshuCreated Sep 8, 2026Updated Sep 8, 2026

Summary

Upgrading satori from 0.32.0 to 0.33.4 turns Inter Bold WOFF text into solid “redacted” bars.

The SVG no longer contains real glyph outlines. Every character is the same five stacked rectangles, only translated along the x-axis. Adjacent stacks overlap, so the rasterised PNG looks like black bars instead of letters.

0.32.0 (same font, same markup, same rasteriser) renders Inter correctly.

This started in 0.33.0, which added HarfBuzz text shaping (https://github.com/vercel/satori/pull/735, b2127fe, 2026-08-20). 0.33.1–0.33.4 did not fix it.

Satori’s README still lists TTF, OTF, and WOFF as supported (not WOFF2). This file is WOFF, not WOFF2, and not a variable font.

Versions

Version Date Changelog Result
0.32.0 2026-08-20 backdrop-filter OK — last good release
0.33.0 2026-08-20 feat: Add HarfBuzz text shaping (#735) broken
0.33.1 2026-08-21 cache / ~12% faster still broken at 0.33.4
0.33.2 2026-08-21 better font caching still broken at 0.33.4
0.33.3 2026-08-21 opacity processing still broken at 0.33.4
0.33.4 2026-08-24 playground Geist tweak broken (confirmed)

0.33.4 still depends on both @shuding/[email protected] and [email protected].

Expected

  • SVG <path d="…"> is real Inter Bold outlines (curves, counters, distinct shapes per letter).
  • Rasterised PNG is readable type.

Actual

  • Satori emits one <path> whose d is only M / L / Z rectangles.
  • There are no cubic or quadratic curves.
  • Every “glyph” is five ~44×9px bars, shifted ~26px, overlapping.

First glyph (satori 0.33.4):

M49.2 60.8 L5.4 60.8 L5.4 51.4 L49.2 51.4 L49.2 60.8Z
M49.2 47.8 L5.4 47.8 L5.4 38.4 L49.2 38.4 L49.2 47.8Z
M49.2 34.8 L5.4 34.8 L5.4 25.4 L49.2 25.4 L49.2 34.8Z
M49.2 21.8 L5.4 21.8 L5.4 12.4 L49.2 12.4 L49.2 21.8Z
M49.2  8.8 L5.4  8.8 L5.4 -0.6 L49.2 -0.6 L49.2  8.8Z

The next characters are the same five bars, translated (~75.2, 101.2, 127.2, …). That shared outline + overlap is why the PNG looks redacted rather than like Inter.

Minimal reproduction (Node, ESM)

javascript
import fs from 'node:fs';
import satori from 'satori';
import { html } from 'satori-html';

const font = fs.readFileSync('./inter-bold.woff'); // Inter Bold, static WOFF (not WOFF2)

const svg = await satori(
  html(`<div style="display:flex;flex-direction:column;width:1200px;height:630px;font-family:Inter;font-weight:700;color:#141a26;font-size:52px;line-height:1.15">Hello Inter</div>`),
  {
    width: 1200,
    height: 630,
    fonts: [
      {
        name: 'Inter',
        data: font,
        weight: 700,
        style: 'normal',
      },
    ],
  },
);

fs.writeFileSync('out.svg', svg);
console.log('curves', (svg.match(/[CcQq]/g) || []).length);
console.log('rects', (svg.match(/L[\d.]+ [\d.]+L[\d.]+ [\d.]+L[\d.]+ [\d.]+Z/g) || []).length);

0.33.4: out.svg is the 5-bar stacks (curves ≈ 0). 0.32.0: same WOFF produces real Inter outlines.

We also rasterised with @resvg/resvg-js. The bars are already in the SVG, so this is not a Resvg bug.

Font used in production: Inter Bold .woff, passed as a Node Buffer:

javascript
fonts: [{ name: 'Inter', data: fs.readFileSync('./inter-bold.woff'), weight: 700, style: 'normal' }]

Environment

  • OS: macOS (Darwin)
  • Node.js: 22.16.0
  • Module system: ESM ("type": "module")
  • Broken: [email protected]
  • Last good: [email protected]
  • [email protected] (HTML string → vnode)
  • Font: Inter Bold, WOFF, weight 700, style normal (not WOFF2, not variable)
  • Canvas: 1200×630
  • Found via eleventy-plugin-og-image (dependency satori@^0.26.0, overridden). Reproduced with a direct satori() call, so the plugin is not required.

What we already ruled out

These still produced the 5-bar paths on 0.33.4:

  1. Template / Yoga / extra CSS — a single inline display:flex div is enough.
  2. line-height1.15 and 60px produced the same rectangle paths.
  3. fflate 0.8.x — the parent app overrides fflate to 0.8.3. Pinning satori to 0.32.0 with that override still in place restored glyphs, so this is not a WOFF inflate break from fflate 0.8. (satori itself declares [email protected].)
  4. Resvg / Sharp — the SVG from satori 0.33 already contains the bars.

Workaround

Pin satori to 0.32.0 (last release before #735):

json
{
  "overrides": {
    "satori": "0.32.0"
  }
}

Debugging notes / questions

These would help isolate the HarfBuzz integration:

  1. WOFF-only, or TTF/OTF too? We did not convert this face to TTF. A same-weight Inter TTF vs WOFF comparison would tell you if outline export is broken for WOFF blobs specifically.
  2. Shared outline for every code point. Every letter is the same 5-rect path, only translated. That looks like one broken outline reused for every glyph ID, not per-glyph decode noise.
  3. Who draws the path? 0.33.4 still depends on both harfbuzzjs and @shuding/opentype.js. Is HarfBuzz returning glyph IDs that then get the wrong outlines from opentype.js, or is hb_draw / outline export itself wrong for WOFF?
  4. WOFF vs glyf. 0.32 used opentype.js for both parse and path. 0.33 added HarfBuzz shaping. If HarfBuzz is given raw WOFF bytes (or decompressed glyf that still needs WOFF transforms), outline export could collapse to this rectangle pattern.

Related

I searched existing issues and did not find one about WOFF glyphs becoming stacked bars after 0.33.

Broken result below: Image

The plugin is being used with 11ty and https://github.com/KiwiKilian/eleventy-plugin-og-image