* fix(router): replace stale optimistic layouts across dynamic params
A detached optimistic shell can commit stale dynamic-layout output before the authoritative payload resolves. Preparing the latter from live router state then makes the shell appear current, so stale server props and BFCache identity survive a cross-param navigation.
All payloads in one navigation must derive reuse identity from the same initiation state. Capture that state once and pass it through commit preparation, while live router state remains the authority for cancellation and commit approval.
Add composition coverage for cross-param replacement, same-param preservation, and layout-owned slots, plus a deterministic browser regression for the prefetched-shell handoff.
* fix(router): require navigation initiation state for payload preparation
Navigation payload callers could omit the initiation state and silently prepare from live router state. A future caller could therefore compile while reintroducing the optimistic-to-authoritative identity bug.
Require the state at both browser-entry and controller boundaries and remove the live-state fallback. Isolated controller tests now choose current-state preparation through an explicitly named test helper.
* test(router): synchronize payload tests on state dispatch
Navigation payload regressions advanced a fixed number of microtasks before reading router state. That coupled the tests to the controller's current async scheduling depth.\n\nExpose a one-shot visible-commit dispatch waiter from the controller harness and await that explicit boundary before assertions.
* docs(router): document the currentState baseline in createPendingNavigationCommit
createPendingNavigationCommit's currentState param has no note on what
it should be. Navigation callers now pass the frozen navigation-initiation
state (per the previous two commits), while the HMR caller still passes
live state, and nothing marks that split as deliberate.
A future navigation call site that passes live state instead of the
initiation state would silently reintroduce the stale cross-param reuse
bug this branch fixes, with no type error to catch it.
Document the invariant on the field so the split reads as intentional.
---------
Co-authored-by: James <james@eli.cx>
* fix(isr): preserve cache headers on initial render
* fix(isr): preserve streaming on initial cache misses
* test(isr): run production lifecycle coverage in CI
* test(isr): wait for asynchronous cache writes
---------
Co-authored-by: James <james@eli.cx>
* fix(app-router): stream generated metadata after the document shell
Dynamic App Router document renders currently await generateMetadata before constructing the page element tree. This delays response headers and the first HTML chunk for streaming-capable clients.
Head resolution coupled metadata and viewport into one awaited result, so the renderer could not suspend only metadata. Start the branches independently, keep blocking callers unchanged, and expose metadata through paired Suspense tag and error outlets. The hidden host wrapper preserves React's shell flush in the presence of hoistable metadata tags.
The focused head test verifies metadata remains pending while viewport resolution completes.
* test(app-router): verify generated metadata follows the production shell
Production coverage only established that HTML eventually arrived, so delaying the first byte until generateMetadata completed remained undetected.
Read the production response incrementally and require the page shell to precede delayed metadata while still asserting the final tags.
* test(app-router): align metadata error coverage with streamed responses
Streaming generateMetadata errors return a recoverable 200 shell before local or global boundaries render after hydration. The compatibility suite incorrectly expected those boundaries and 500 statuses in raw server HTML, which conflicts with Next.js 16.2.7 and fails the integration shard.
Update raw-response assertions to cover shell behavior and add browser coverage for page and layout metadata errors with and without local boundaries.
* fix(app-router): stream metadata during navigation
* fix(app-router): isolate connection probes from streaming metadata
Dynamic metadata prefetches can leave Flight responses open indefinitely when generateMetadata calls connection(). Streaming starts metadata in parallel with page classification, but the speculative probe mutated shared request state and captured the sibling metadata branch.
Run probes in a nested request scope, then propagate dynamic usage and new diagnostic errors back with concurrency-safe rules. This preserves classification while allowing sibling metadata work to complete.
* refactor(app-router): isolate fallback metadata planning
Streamed metadata fallbacks previously configured the general head resolver with traversal flags for boundary repetition, leaf ordering, and viewport suppression. That made fallback policy part of normal metadata resolution and obscured the RSC navigation transport contract.
Build an explicit HTTP-access fallback metadata source plan, then resolve it through the shared ordered metadata merger. Add focused planner coverage and pin delayed navigation redirects to HTTP 200 Flight digest transport.
* fix(app-router): preserve not-found metadata search params
Page-local not-found metadata lost searchParams after deferred metadata called notFound(), so query-derived tags were wrong and search access was invisible to dynamic-usage tracking. Terminal fallback rendering also recomputed the boundary head without the normalized query.
Classify not-found ownership from module identity and tree position, attach query state and its observer only for page-owned conventions, and thread normalized search params through terminal fallback rendering. Cover repeated fallback leaves, observer access, and a production page-local not-found route.
* fix(app-router): release completed connection probes
Async work created inside a speculative connection probe retained the child request store after the probe returned. Because that store still referenced the completed probe, a later connection() call suspended forever.
Restore the child store's currently inherited probe during deterministic cleanup. This preserves nested probe ownership while allowing late continuations to observe the completed scope, with a real AsyncLocalStorage regression covering the lifecycle.
* fix(app-router): preserve deferred metadata cache signals
Deferred metadata dynamic usage can overlap a speculative layout probe. The probe cleared and later consumed the shared request flag, allowing an RSC cache entry to be written even though the completed response was marked no-store.
The layout classifier treated save-and-restore mutation as async isolation. Run probe classification in a child dynamic-usage scope so sibling metadata retains the parent request state, and cover the overlap through the dispatch cache boundary.
* fix(app-router): preserve fallback metadata parity
---------
Co-authored-by: James <james@eli.cx>
* fix(navigation): tolerate history state writes rejected by the browser
history.pushState/replaceState can throw a SecurityError when document.URL
still contains userinfo credentials (e.g. behind HTTP Basic auth Chromium
strips them from location.href but not from document.URL), in sandboxed or
opaque-origin documents, or when the per-origin call quota is exceeded.
The notify-suppressed history primitives only persist vinext navigation
metadata, so an unguarded call let a hydration-time replaceState failure
escalate to the global error boundary and blank the whole page. Wrap both
primitives in try/catch and log a warning so metadata-write failures degrade
gracefully.
Fixes#2614
* fix(navigation): avoid rewriting URL for history metadata
---------
Co-authored-by: James <james@eli.cx>
* fix(shims): align public API with vendored Next types
* fix(shims): preserve revalidation and runtime behavior
* test(shims): align cache revalidation expectations
* fix(ci): package types for deploy suite
* fix(app-router): hoist streamed metadata into <head> instead of body
Async generateMetadata was serialized via dangerouslySetInnerHTML into a
hidden <div> in the body, which React cannot hoist. For JS-capable clients
(browsers, Googlebot) the tags stayed in <body>, where Google ignores
rel=canonical, hreflang and robots.
Render the resolved metadata as real <title>/<meta>/<link> elements through
MetadataHead so React 19 hoists them into <head>, matching Next.js. Remove
the now-unused renderMetadataToHtml string serializer.
* Fix tests
* fix(app-router): harden metadata hoisting coverage
* fix(app-router): restore streamed metadata placement
* fix(app-router): preserve static metadata placement
---------
Co-authored-by: James <james@eli.cx>
The dev overlay recovery spec armed its reload canary after only confirming that the navigation runtime existed. That was not enough to prove the app-router client tree had hydrated the Link handler, so CI could occasionally follow the anchor as a hard navigation before the soft-nav path was ready.
Reuse the shared app-router hydration helper before setting the canary and clicking the recovery link. The test still exercises the real browser click path, but no longer races client hydration.