Skip to content

Turbopack: dynamic metadata images (opengraph-image.tsx) fail with async module dependencies #91676

@franky47

Description

@franky47

Link to the code that reproduces this issue

https://github.com/franky47/next-16.2.0-canary.0-repro

To Reproduce

git clone https://github.com/franky47/next-16.2.0-canary.0-repro
cd next-16.2.0-canary.0-repro
pnpm install
pnpm build            # Fails with Turbopack (default)
pnpm build --webpack  # Works

A dynamic route app/blog/[slug] has an opengraph-image.tsx that imports from app/data.ts, which uses top-level await (making it an async module). Any module that transitively depends on async modules (content collections, MDX processing, etc.) will trigger this.

Current vs. Expected behavior

Current: Build fails during static page generation with:

Error occurred prerendering page "/blog/some-slug"
TypeError: Cannot read properties of undefined (reading 'default')
    at collectStaticImagesFiles (resolve-metadata.ts)

Expected: Build succeeds, as it does with next build --webpack or on Next.js 16.1.7.

Provide environment information

Operating System:
  Platform: darwin
  Arch: arm64
  Version: Darwin Kernel Version 24.6.0: Mon Jan 19 22:01:58 PST 2026; root:xnu-11417.140.69.708.3~1/RELEASE_ARM64_T6041
  Available memory (MB): 131072
  Available CPU cores: 16
Binaries:
  Node: 24.11.0
  npm: 11.6.2
  Yarn: 1.22.22
  pnpm: 10.27.0
Relevant Packages:
  next: 16.2.1-canary.1 // Latest available version is detected (16.2.1-canary.1).
  eslint-config-next: N/A
  react: 19.2.4
  react-dom: 19.2.4
  typescript: 5.9.3
Next.js Config:
  output: N/A

Which area(s) are affected? (Select all that apply)

Turbopack

Which stage(s) are affected? (Select all that apply)

next build (local)

Additional context

I detected this issue when upgrading to [email protected] in the nuqs docs app.

Root cause

Bisected to PR #88487 ("Turbopack: retain loader tree order for metadata"), which shipped in 16.2.0-canary.0.

That PR changed how Turbopack references dynamic metadata image modules in the app-page template (crates/next-core/src/app_page_loader_tree.rs):

Before (16.1.7, working):

import __TURBOPACK__openGraph__5__ from "METADATA_5";
// ...
metadata: { openGraph: [__TURBOPACK__openGraph__5__] }

After (16.2.0, broken):

const __TURBOPACK__openGraph__5__ = require("METADATA_5");
// ...
metadata: { openGraph: [__TURBOPACK__openGraph__5__.default] }

The require() is synchronous, but the metadata module can be async when opengraph-image.tsx transitively depends on async modules (top-level await, async module initialization from libraries like fumadocs-mdx, content collections, etc.). The synchronous require() returns before async initialization completes, so .default is undefined. The route tree is then constructed with [undefined] in the metadata arrays.

Later, collectStaticImagesFiles in resolve-metadata.ts iterates the array and calls interopDefault(undefined)undefined.default → TypeError.

Why webpack is unaffected: The webpack code path in metadata/discover.ts wraps dynamic metadata images in async factory functions:

(async (props) => (await import(/* webpackMode: "eager" */ "...")).default(props))

The .default access is deferred until the async import resolves.

Why static images are unaffected in Turbopack: write_static_metadata_item in the same Rust file already wraps static images in async factory functions. Only write_metadata_item for MetadataWithAltItem::Dynamic uses the broken synchronous pattern.

Suggested fix (from Opus 4.6)

Wrap dynamic metadata items in async factory functions (matching both the webpack behavior and the existing Turbopack write_static_metadata_item behavior), so .default is accessed after async initialization completes:

// Instead of:
const mod = require("METADATA_5");
metadata: { openGraph: [mod.default] }

// Wrap in an async factory:
metadata: { openGraph: [async (props) => {
  const mod = await import("METADATA_5");
  return mod.default(props);
}] }

Bisect results

Version Result
16.1.7 pass
16.2.0-canary.0 fail
16.2.0 fail

Additional context

  • PR Turbopack: retain loader tree order for metadata #88487 was fixing #87322 (infinite compilation loop caused by ESM import hoisting breaking loader tree order). The fix is correct for that issue, but the switch to synchronous require() + immediate .default doesn't account for async metadata modules.
  • Any project using content layer tools (fumadocs-mdx, Contentlayer, etc.) that compile MDX at build time will likely hit this, since the compiled MDX modules are async under Turbopack.

Metadata

Metadata

Assignees

No one assigned

    Labels

    AfterRelated to the after() function.MetadataRelated to Next.js' Metadata API.TurbopackRelated to Turbopack with Next.js.linear: turbopackConfirmed issue that is tracked by the Turbopack team.

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions