cloudflare / cloudflare/vinext

App Router cacheComponents: skip metadata/viewport in error recovery RSC payload (defer to client hydration)

Open
#1,696 0 comments 0 reactions 0 assignees View on GitHub
nextjs-tracking
Dominant language
TypeScript
Stars
8.8k
Forks
406
Avg merge
2d 6h
Merged PRs (30d)
120

Description

## Next.js Change

**Commit:** [`79f9e67`](https://github.com/vercel/next.js/commit/79f9e67e23039476e24c5942be0a2d4c23581435)
**PR:** [#94134 — Exclude static metadata and viewport from error recovery prerender when cache-components is enabled](https://github.com/vercel/next.js/pull/94134)

## What changed

`getErrorRSCPayload` in `packages/next/src/server/app-render/app-render.tsx` gains a new `shouldRenderMetadataAndViewport` parameter. When `false`, the function skips constructing the `Viewport` and `Metadata` components entirely and emits `null` slots for them in the error payload's `initialHead`:

```diff
async function getErrorRSCPayload(
tree: LoaderTree,
ctx: AppRenderContext,
ssrError: unknown,
- errorType: MetadataErrorType | 'redirect' | undefined
+ errorType: MetadataErrorType | 'redirect' | undefined,
+ shouldRenderMetadataAndViewport: boolean
)
```

```diff
- const { Viewport, Metadata } = createMetadataComponents({ ... })
+ let Viewport: ComponentType | null = null
+ let Metadata: ComponentType | null = null
+ if (shouldRenderMetadataAndViewport) {
+ const metadataComponents = createMetadataComponents({ ... })
+ Viewport = metadataComponents.Viewport
+ Metadata = metadataComponents.Metadata
+ }
```

```diff
- createElement(Viewport, null),
+ Viewport ? createElement(Viewport, null) : null,
...
- createElement(Metadata, null)
+ Metadata ? createElement(Metadata, null) : null
```

Two call sites of `getErrorRSCPayload` (the two error-recovery `renderToStream` paths) now pass `true` for **normal error rendering**. A third call site (the cache-components prerender error recovery path) passes `false` so that dynamic `generateMetadata` / `generateViewport` do not block producing a shell.

## Rationale (from PR description)

> These functions can be dynamic however in this mode we are trying to recover on the client so there is little point in attempting to resolve the http error recovery metadata since it might block us producing a shell. We now exclude these from the error recovery RSC payload since we will recover the metadata on hydration in the browser.

## Test coverage added

- `test/e2e/app-dir/cache-components-errors/cache-components-errors.http-access-fallback-prerender.test.ts` — new `describe('notFound() with dynamic metadata and viewport', ...)` block at `/not-found-dynamic-head/[slug]` asserts the prerendered `not-found.html` does **not** contain the dynamic title/description/themeColor markers from `not-found.tsx`, but after `next start` the browser DOM **does** contain them (recovered on hydration).
- New fixture `fixtures/http-access-fallback-prerender/app/not-found-dynamic-head/[slug]/` with `page.tsx` (`unstable_instant = false`, calls `notFound()`, has static `metadata` and async `generateViewport`) and `not-found.tsx` (async `generateMetadata` + `generateViewport`).
- `test/e2e/app-dir/parallel-routes-not-found/parallel-routes-not-found.test.ts` updated with a `retry` + DOM title assertion (`'404: This page could not be found.'`) and a TODO comment noting that SSR vs hydration-data title disagreement when cache-components is off is a sign of incoherent notFound title handling — flagged as a pre-existing parity gap.

## Impact on vinext

vinext's App Router execution pipeline in `packages/vinext/src/server/app-page-execution.ts` models an `http-access-fallback` outcome and runs an error-recovery path that constructs a fallback RSC payload. If that path currently invokes `createMetadataComponents` (or vinext's equivalent metadata/viewport rendering) inside the recovery payload, an `async generateMetadata` / `generateViewport` in a `not-found.tsx` can block shell emission for cache-components routes — diverging from the new upstream behavior.

Concrete things vinext should mirror:

1. **Gate metadata/viewport rendering in the error recovery payload** behind a `shouldRenderMetadataAndViewport` flag (or equivalent) and pass `false` for the cache-components prerender error recovery call site so the shell can be produced without awaiting dynamic metadata.
2. **Emit `null` head slots** (not skipped React elements that change keys) so the client hydration path still matches.
3. **Recover metadata on hydration**: client navigation/hydration should re-resolve dynamic `generateMetadata` / `generateViewport` from `not-found.tsx` and patch the DOM (`document.title`, `meta[name="description"]`, `meta[name="theme-color"]`).
4. **Preserve normal (non-cache-components) error rendering behavior**: the two normal error `renderToStream` call sites still pass `true`, so this is a cache-components-only behavior change.
5. Port the e2e test fixture and assertions, especially the post-hydration DOM check, so we don't regress.

## Related

- #1615 — App Router cacheComponents: revisit notFound recovery (broader resumable-error-page refactor this PR builds on)
- #764 — closed; tracked the now-reverted boundary-rerender approach

Contributor guide

Open the contributing guide

Research direction

Start in packages/vinext/src/server/app-page-execution.ts and trace the http-access-fallback error-recovery path and its fallback RSC payload. Compare the metadata handling with packages/next/src/server/app-render/app-render.tsx, then run test/e2e/app-dir/cache-components-errors/cache-components-errors.http-access-fallback-prerender.test.ts. Done means dynamic metadata does not block the prerendered shell, hydration restores the head, and normal error rendering remains unchanged.

Written by the indexing model from the issue text.

Assessment

Tech stack
next.js, react, typescript
Domain
backend, frontend, web-dev
Issue type
Feature
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Clearly specified
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.