#791·satori

0.33 harfbuzzjs hb.wasm fails to resolve when satori is bundled (Next.js/Turbopack)

Author: piotrkulpinskiCreated Aug 25, 2026Updated Aug 31, 2026

Summary

Since 0.33.0 added HarfBuzz shaping (#735), satori depends on harfbuzzjs, whose emscripten loader reads hb.wasm from a path next to its own file. When a bundler processes satori's server code, that path is rewritten and the wasm is never emitted, so shaping fails at build and at runtime.

Environment

  • satori 0.33.3
  • harfbuzzjs 0.10.0
  • Next.js 16.3.2 with Turbopack
  • Node 22, production next build

What happens

next build compiles, then fails while rendering routes that call satori() (in our case the OpenGraph and badge image routes):

failed to asynchronously prepare wasm: Error: ENOENT: no such file or directory,
open '/ROOT/node_modules/harfbuzzjs/hb.wasm'
Aborted(Error: ENOENT: no such file or directory, open '/ROOT/node_modules/harfbuzzjs/hb.wasm')
RuntimeError: Aborted(Error: ENOENT ... /ROOT/node_modules/harfbuzzjs/hb.wasm).

/ROOT is Turbopack's virtual build root. The real file sits at node_modules/harfbuzzjs/hb.wasm, next to hb.js, but the bundled code looks for it under the virtual root, where nothing was copied. Pinning satori back to 0.32.x (before the HarfBuzz change) removes the error.

Cause

harfbuzzjs locates its wasm with emscripten's locateFile, resolved against __dirname / a path relative to hb.js. Turbopack (and webpack) trace imports statically and cannot follow that runtime path, so hb.wasm is not traced into the build output.

Workaround

Mark the package external so the bundler leaves it in node_modules, where the loader finds the wasm:

typescript
// next.config.ts
const nextConfig = {
  serverExternalPackages: ["harfbuzzjs"],
}

With this set, next build completes and shaping works at runtime.

Ask

Either of these would help downstream users:

  1. Document the serverExternalPackages: ["harfbuzzjs"] step for Next.js in the satori README or the 0.33 release notes.
  2. Reference the wasm in a form bundlers can trace (for example new URL("hb.wasm", import.meta.url)) so no manual config is needed.

Glad to open a docs PR if option 1 is the preferred path.