nextjs - ✅(Solved) Fix Turbopack: dynamic metadata images (opengraph-image.tsx) fail with async module dependencies [4 pull requests, 1 comments, 2 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
vercel/next.js#91676Fetched 2026-04-08 01:02:33
View on GitHub
Comments
1
Participants
2
Timeline
17
Reactions
2
Author
Timeline (top)
labeled ×5cross-referenced ×4referenced ×3closed ×1

Error Message

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

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.

<details> <summary>

Suggested fix (from Opus 4.6)

</summary>

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);
}] }
</details>

Fix Action

Fixed

PR fix notes

PR #1371: chore: update Next.js to 16.2.0

Description (problem / solution / changelog)

Summary

Changed files

  • pnpm-lock.yaml (modified, +196/-194)
  • pnpm-workspace.yaml (modified, +1/-1)

PR #1706: chore: update deps

Description (problem / solution / changelog)

blocked by https://github.com/vercel/next.js/issues/91676

Changed files

  • .github/workflows/validate.yaml (modified, +8/-7)
  • app/(app)/(default)/_components/sticky-header.tsx (modified, +1/-1)
  • components/content/callout.tsx (modified, +1/-1)
  • components/content/quiz-controls.tsx (modified, +1/-1)
  • components/content/quiz-form.tsx (modified, +1/-1)
  • components/link.tsx (modified, +23/-86)
  • components/page-title.tsx (modified, +1/-3)
  • lib/content/keystatic/components/callout/preview.tsx (modified, +1/-1)
  • next.config.ts (modified, +3/-1)
  • package.json (modified, +44/-56)
  • pnpm-lock.yaml (modified, +3596/-3499)

PR #91700: test: add failing test for #91676

Description (problem / solution / changelog)

Adds a failing reproduction test for #91676.

Looking for a relevant fix (I had Opus generate something in the Rust code but want to make sure it's relevant first), I'll update this PR when it passes all tests locally.

Changed files

  • test/e2e/app-dir/metadata-dynamic-routes-async-deps/app/blog/[slug]/opengraph-image.tsx (added, +22/-0)
  • test/e2e/app-dir/metadata-dynamic-routes-async-deps/app/blog/[slug]/page.tsx (added, +14/-0)
  • test/e2e/app-dir/metadata-dynamic-routes-async-deps/app/data.ts (added, +9/-0)
  • test/e2e/app-dir/metadata-dynamic-routes-async-deps/app/layout.tsx (added, +8/-0)
  • test/e2e/app-dir/metadata-dynamic-routes-async-deps/index.test.ts (added, +23/-0)
  • test/e2e/app-dir/metadata-dynamic-routes-async-deps/next.config.js (added, +2/-0)

PR #91705: Turbopack: lazy require metadata and handle TLA

Description (problem / solution / changelog)

A regression from #88487

  1. Make metadata import lazy with () => require(), just like for the layout segments
  2. Properly await the return value to better handle TLA modules

This align with Webpack which does this:

<img width="1535" height="509" alt="Bildschirmfoto 2026-03-20 um 11 58 00" src="https://github.com/user-attachments/assets/f2864c86-ccce-4884-8417-c8ae06c05f78" />

Closes PACK-6927 Closes #91700 Closes https://github.com/vercel/next.js/issues/91676

Changed files

  • crates/next-core/src/app_page_loader_tree.rs (modified, +62/-47)
  • packages/next/src/lib/metadata/resolve-metadata.ts (modified, +2/-3)
  • test/e2e/app-dir/metadata-dynamic-routes-async-deps/app/blog/[slug]/opengraph-image.tsx (added, +22/-0)
  • test/e2e/app-dir/metadata-dynamic-routes-async-deps/app/blog/[slug]/page.tsx (added, +14/-0)
  • test/e2e/app-dir/metadata-dynamic-routes-async-deps/app/data.ts (added, +9/-0)
  • test/e2e/app-dir/metadata-dynamic-routes-async-deps/app/layout.tsx (added, +8/-0)
  • test/e2e/app-dir/metadata-dynamic-routes-async-deps/index.test.ts (added, +22/-0)
  • test/e2e/app-dir/metadata-dynamic-routes-async-deps/next.config.js (added, +2/-0)

Code Example

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

---

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

---

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

---

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

---

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

---

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

---

// 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);
}] }
RAW_BUFFERClick to expand / collapse

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.

<details> <summary>

Suggested fix (from Opus 4.6)

</summary>

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);
}] }
</details>

Bisect results

VersionResult
16.1.7pass
16.2.0-canary.0fail
16.2.0fail

Additional context

  • PR #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.

extent analysis

Fix Plan

To resolve the issue, we need to modify the Turbopack code to handle dynamic metadata items with async factory functions.

  • Update the write_metadata_item function in crates/next-core/src/app_page_loader_tree.rs to wrap dynamic metadata items in async factory functions.
  • Replace the synchronous require() with an async import() to ensure that the metadata module is fully initialized before accessing its default export.

Example code changes:

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

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

This change will ensure that the .default access is deferred until the async import resolves, fixing the TypeError: Cannot read properties of undefined (reading 'default') error.

Verification

To verify that the fix worked, run the following commands:

pnpm build

The build should succeed without any errors. You can also test the build with the --webpack flag to ensure that the fix does not introduce any regressions:

pnpm build --webpack

Extra Tips

  • When working with async metadata modules, it's essential to use async factory functions to ensure that the modules are fully initialized before accessing their exports.
  • The fix should be applied to the Turbopack code to ensure that dynamic metadata items are handled correctly.
  • If you're using content layer tools like fumadocs-mdx or Contentlayer, make sure to update your code to use async factory functions when compiling MDX at build time.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

nextjs - ✅(Solved) Fix Turbopack: dynamic metadata images (opengraph-image.tsx) fail with async module dependencies [4 pull requests, 1 comments, 2 participants]