Skip to content

[metadata] skip head cache in default slot #78206

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Apr 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { FlightRouterState } from '../../../../server/app-render/types'
import type { CacheNode } from '../../../../shared/lib/app-router-context.shared-runtime'
import { DEFAULT_SEGMENT_KEY } from '../../../../shared/lib/segment'
import { createRouterCacheKey } from '../create-router-cache-key'

export function findHeadInCache(
Expand All @@ -22,28 +23,22 @@ function findHeadInCacheImpl(

// First try the 'children' parallel route if it exists
// when starting from the "root", this corresponds with the main page component
if (parallelRoutes.children) {
const [segment, childParallelRoutes] = parallelRoutes.children
const childSegmentMap = cache.parallelRoutes.get('children')
if (childSegmentMap) {
const cacheKey = createRouterCacheKey(segment)
const cacheNode = childSegmentMap.get(cacheKey)
if (cacheNode) {
const item = findHeadInCacheImpl(
cacheNode,
childParallelRoutes,
keyPrefix + '/' + cacheKey
)
if (item) return item
}
}
}
const parallelRoutesKeys = Object.keys(parallelRoutes).filter(
(key) => key !== 'children'
)

// if we didn't find metadata in the page slot, check the other parallel routes
for (const key in parallelRoutes) {
if (key === 'children') continue // already checked above
// if we are at the root, we need to check the children slot first
if ('children' in parallelRoutes) {
parallelRoutesKeys.unshift('children')
}

for (const key of parallelRoutesKeys) {
const [segment, childParallelRoutes] = parallelRoutes[key]
// If the parallel is not matched and using the default segment,
// skip searching the head from it.
if (segment === DEFAULT_SEGMENT_KEY) {
continue
}
const childSegmentMap = cache.parallelRoutes.get(key)
if (!childSegmentMap) {
continue
Expand Down
8 changes: 5 additions & 3 deletions packages/next/src/lib/metadata/metadata.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -206,9 +206,11 @@ export function createMetadataComponents({
const promise = resolveFinalMetadata()
if (serveStreamingMetadata) {
return (
<Suspense fallback={null}>
<AsyncMetadata promise={promise} />
</Suspense>
<div hidden>
<Suspense fallback={null}>
<AsyncMetadata promise={promise} />
</Suspense>
</div>
)
}
const metadataState = await promise
Expand Down
76 changes: 13 additions & 63 deletions packages/next/src/server/app-render/app-render.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,8 @@ interface ParseRequestHeadersOptions {
}

const flightDataPathHeadKey = 'h'
const getFlightViewportKey = (requestId: string) => requestId + 'v'
const getFlightMetadataKey = (requestId: string) => requestId + 'm'

interface ParsedRequestHeaders {
/**
Expand Down Expand Up @@ -336,30 +338,6 @@ function createNotFoundLoaderTree(loaderTree: LoaderTree): LoaderTree {
]
}

function createDivergedMetadataComponents(
Metadata: React.ComponentType,
serveStreamingMetadata: boolean
): {
StaticMetadata: React.ComponentType<{}>
StreamingMetadata: React.ComponentType<{}> | null
} {
function EmptyMetadata() {
return null
}
const StreamingMetadata: React.ComponentType | null = serveStreamingMetadata
? Metadata
: null

const StaticMetadata: React.ComponentType<{}> = serveStreamingMetadata
? EmptyMetadata
: Metadata

return {
StaticMetadata,
StreamingMetadata,
}
}

/**
* Returns a function that parses the dynamic segment and return the associated value.
*/
Expand Down Expand Up @@ -527,14 +505,6 @@ async function generateDynamicRSCPayload(
serveStreamingMetadata,
})

const { StreamingMetadata, StaticMetadata } =
createDivergedMetadataComponents(() => {
return (
// Adding requestId as react key to make metadata remount for each render
<MetadataTree key={requestId} />
)
}, serveStreamingMetadata)

flightData = (
await walkTreeWithFlightRouterState({
ctx,
Expand All @@ -551,9 +521,9 @@ async function generateDynamicRSCPayload(
isPossibleServerAction={ctx.isPossibleServerAction}
/>
{/* Adding requestId as react key to make metadata remount for each render */}
<ViewportTree key={requestId} />
{StreamingMetadata ? <StreamingMetadata /> : null}
<StaticMetadata />
<ViewportTree key={getFlightViewportKey(requestId)} />
{/* Not add requestId as react key to ensure segment prefetch could result consistently if nothing changed */}
<MetadataTree key={getFlightMetadataKey(requestId)} />
</React.Fragment>
),
injectedCSS: new Set(),
Expand Down Expand Up @@ -857,14 +827,6 @@ async function getRSCPayload(

const preloadCallbacks: PreloadCallbacks = []

const { StreamingMetadata, StaticMetadata } =
createDivergedMetadataComponents(() => {
return (
// Not add requestId as react key to ensure segment prefetch could result consistently if nothing changed
<MetadataTree />
)
}, serveStreamingMetadata)

const seedData = await createComponentTree({
ctx,
loaderTree: tree,
Expand All @@ -878,7 +840,6 @@ async function getRSCPayload(
missingSlots,
preloadCallbacks,
authInterrupts: ctx.renderOpts.experimental.authInterrupts,
StreamingMetadata,
StreamingMetadataOutlet,
})

Expand All @@ -896,8 +857,9 @@ async function getRSCPayload(
statusCode={ctx.res.statusCode}
isPossibleServerAction={ctx.isPossibleServerAction}
/>
<ViewportTree key={ctx.requestId} />
<StaticMetadata />
<ViewportTree key={getFlightViewportKey(ctx.requestId)} />
{/* Not add requestId as react key to ensure segment prefetch could result consistently if nothing changed */}
<MetadataTree />
</React.Fragment>
)

Expand Down Expand Up @@ -984,16 +946,8 @@ async function getErrorRSCPayload(
serveStreamingMetadata: serveStreamingMetadata,
})

const { StreamingMetadata, StaticMetadata } =
createDivergedMetadataComponents(
() => (
<React.Fragment key={flightDataPathHeadKey}>
{/* Adding requestId as react key to make metadata remount for each render */}
<MetadataTree key={requestId} />
</React.Fragment>
),
serveStreamingMetadata
)
// {/* Adding requestId as react key to make metadata remount for each render */}
const metadata = <MetadataTree key={getFlightMetadataKey(requestId)} />

const initialHead = (
<React.Fragment key={flightDataPathHeadKey}>
Expand All @@ -1003,12 +957,11 @@ async function getErrorRSCPayload(
isPossibleServerAction={ctx.isPossibleServerAction}
/>
{/* Adding requestId as react key to make metadata remount for each render */}
<ViewportTree key={requestId} />
<ViewportTree key={getFlightViewportKey(requestId)} />
{process.env.NODE_ENV === 'development' && (
<meta name="next-error" content="not-found" />
)}
{StreamingMetadata ? <StreamingMetadata /> : null}
<StaticMetadata />
{metadata}
</React.Fragment>
)

Expand All @@ -1028,10 +981,7 @@ async function getErrorRSCPayload(
const seedData: CacheNodeSeedData = [
initialTree[0],
<html id="__next_error__">
<head>
{StreamingMetadata ? <StreamingMetadata /> : null}
<StaticMetadata />
</head>
<head>{metadata}</head>
<body>
{process.env.NODE_ENV !== 'production' && err ? (
<template
Expand Down
11 changes: 0 additions & 11 deletions packages/next/src/server/app-render/create-component-tree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ export function createComponentTree(props: {
missingSlots?: Set<string>
preloadCallbacks: PreloadCallbacks
authInterrupts: boolean
StreamingMetadata: React.ComponentType | null
StreamingMetadataOutlet: React.ComponentType
}): Promise<CacheNodeSeedData> {
return getTracer().trace(
Expand Down Expand Up @@ -76,7 +75,6 @@ async function createComponentTreeInternal({
missingSlots,
preloadCallbacks,
authInterrupts,
StreamingMetadata,
StreamingMetadataOutlet,
}: {
loaderTree: LoaderTree
Expand All @@ -91,7 +89,6 @@ async function createComponentTreeInternal({
missingSlots?: Set<string>
preloadCallbacks: PreloadCallbacks
authInterrupts: boolean
StreamingMetadata: React.ComponentType | null
StreamingMetadataOutlet: React.ComponentType | null
}): Promise<CacheNodeSeedData> {
const {
Expand Down Expand Up @@ -390,7 +387,6 @@ async function createComponentTreeInternal({

// Resolve the segment param
const actualSegment = segmentParam ? segmentParam.treeSegment : segment
const metadata = StreamingMetadata ? <StreamingMetadata /> : undefined

// Use the same condition to render metadataOutlet as metadata
const metadataOutlet = StreamingMetadataOutlet ? (
Expand Down Expand Up @@ -513,7 +509,6 @@ async function createComponentTreeInternal({
missingSlots,
preloadCallbacks,
authInterrupts,
StreamingMetadata: isChildrenRouteKey ? StreamingMetadata : null,
// `StreamingMetadataOutlet` is used to conditionally throw. In the case of parallel routes we will have more than one page
// but we only want to throw on the first one.
StreamingMetadataOutlet: isChildrenRouteKey
Expand Down Expand Up @@ -699,12 +694,6 @@ async function createComponentTreeInternal({
actualSegment,
<React.Fragment key={cacheNodeKey}>
{pageElement}
{/*
* The order here matters since a parent might call findDOMNode().
* findDOMNode() will return the first child if multiple children are rendered.
* But React will hoist metadata into <head> which breaks scroll handling.
*/}
{metadata}
{layerAssets}
<OutletBoundary>
<MetadataOutlet ready={getViewportReady} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,6 @@ export async function walkTreeWithFlightRouterState({
getMetadataReady,
preloadCallbacks,
authInterrupts: experimental.authInterrupts,
StreamingMetadata: null,
StreamingMetadataOutlet,
}
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ export default function Root({ children }: { children: ReactNode }) {
{`to /parallel-routes-default`}
</Link>
<br />

<Link href="/parallel-routes-no-children" id="to-no-children">
{`to /parallel-routes-no-children`}
</Link>
<br />
</div>
{children}
</body>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Page() {
return 'default @bar'
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default function Page() {
return <p id="bar-page">test-page @bar - 1</p>
}

export const metadata = {
title: 'first page - @bar',
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export default function Layout({ children }) {
return (
<div>
<h2>@bar Layout</h2>
<div id="bar-children">{children}</div>
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Page() {
return 'page @bar'
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default function Page() {
return <p id="bar-page">test-page @bar - 2</p>
}

export const metadata = {
title: 'second page - @bar',
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Page() {
return 'default @foo'
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export default function Layout({ children }) {
return (
<div>
<h2>@foo Layout</h2>
<div id="foo-children">{children}</div>
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Page() {
return 'no-bar @foo'
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Page() {
return 'page @foo'
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Page() {
return 'test-page @foo'
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { connection } from 'next/server'
import Link from 'next/link'

// skip rendering children
export default function Layout({ bar, foo }) {
return (
<div>
<h1>Parallel Routes Layout - No Children</h1>

<Link href="/parallel-routes-no-children/first" id="to-no-children-first">
{`to /parallel-routes-no-children/first`}
</Link>
<br />
<Link
href="/parallel-routes-no-children/second"
id="to-no-children-second"
>
{`to /parallel-routes-no-children/second`}
</Link>
<br />

<div id="foo-slot">{foo}</div>
<div id="bar-slot">{bar}</div>
</div>
)
}

export async function generateMetadata() {
await connection()
await new Promise((resolve) => setTimeout(resolve, 300))
return {
title: 'parallel-routes-default layout title',
}
}
Loading
Loading