Skip to content
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

[metadata]: ensure metadata boundary is only rendered once on client nav #76692

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
Expand Up @@ -19,7 +19,30 @@ function findHeadInCacheImpl(
// Returns the entire Cache Node of the segment whose head we will render.
return [cache, keyPrefix]
}

// 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
}
}
}

// 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

const [segment, childParallelRoutes] = parallelRoutes[key]
const childSegmentMap = cache.parallelRoutes.get(key)
if (!childSegmentMap) {
Expand Down
2 changes: 1 addition & 1 deletion packages/next/src/server/app-render/app-render.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -522,6 +522,7 @@ async function generateDynamicRSCPayload(
<NonIndex ctx={ctx} />
{/* Adding requestId as react key to make metadata remount for each render */}
<ViewportTree key={requestId} />
{StreamingMetadata ? <StreamingMetadata /> : null}
<StaticMetadata />
</React.Fragment>
),
Expand All @@ -532,7 +533,6 @@ async function generateDynamicRSCPayload(
getViewportReady,
getMetadataReady,
preloadCallbacks,
StreamingMetadata,
StreamingMetadataOutlet,
})
).map((path) => path.slice(1)) // remove the '' (root) segment
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ export async function walkTreeWithFlightRouterState({
getMetadataReady,
ctx,
preloadCallbacks,
StreamingMetadata,
StreamingMetadataOutlet,
}: {
loaderTreeToFilter: LoaderTree
Expand All @@ -56,8 +55,7 @@ export async function walkTreeWithFlightRouterState({
getViewportReady: () => Promise<void>
ctx: AppRenderContext
preloadCallbacks: PreloadCallbacks
StreamingMetadata: React.ComponentType<{}> | null
StreamingMetadataOutlet: React.ComponentType<{}>
StreamingMetadataOutlet: React.ComponentType
}): Promise<FlightDataPath[]> {
const {
renderOpts: { nextFontManifest, experimental },
Expand Down Expand Up @@ -206,7 +204,7 @@ export async function walkTreeWithFlightRouterState({
getMetadataReady,
preloadCallbacks,
authInterrupts: experimental.authInterrupts,
StreamingMetadata,
StreamingMetadata: null,
StreamingMetadataOutlet,
}
)
Expand Down Expand Up @@ -267,7 +265,6 @@ export async function walkTreeWithFlightRouterState({
getViewportReady,
getMetadataReady,
preloadCallbacks,
StreamingMetadata,
StreamingMetadataOutlet,
})

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Page() {
return 'test-page @bar'
}
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 'test-page @foo'
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { connection } from 'next/server'

export default function TestPage() {
return 'test page'
}

export async function generateMetadata() {
await connection()
await new Promise((resolve) => setTimeout(resolve, 3000))
return {
title: `Dynamic api ${Math.random()}`,
}
}
12 changes: 11 additions & 1 deletion test/e2e/app-dir/metadata-streaming/app/parallel-routes/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
import Link from 'next/link'

export default function Page() {
return <div>Hello from Nested</div>
return (
<div>
Hello from Nested{' '}
<Link href="/parallel-routes/test-page">
To /parallel-routes/test-page
</Link>
<Link href="/parallel-routes/no-bar">To /parallel-routes/no-bar</Link>
</div>
)
}

export const metadata = {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { connection } from 'next/server'

export default function TestPage() {
return 'test page'
}

export async function generateMetadata() {
await connection()
await new Promise((resolve) => setTimeout(resolve, 3000))
return {
title: `Dynamic api ${Math.random()}`,
}
}
26 changes: 25 additions & 1 deletion test/e2e/app-dir/metadata-streaming/metadata-streaming.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,14 +88,38 @@ describe('app-dir - metadata-streaming', () => {
expect((await browser.elementsByCss('body meta')).length).toBe(9)
})

it('should only insert metadata once for parallel routes', async () => {
it('should only insert metadata once for parallel routes when slots match', async () => {
const browser = await next.browser('/parallel-routes')

expect((await browser.elementsByCss('head title')).length).toBe(1)
expect((await browser.elementsByCss('body title')).length).toBe(0)

const $ = await next.render$('/parallel-routes')
expect($('title').length).toBe(1)

// validate behavior remains the same on client navigations
await browser.elementByCss('[href="/parallel-routes/test-page"]').click()

await retry(async () => {
expect(await browser.elementByCss('title').text()).toContain(
'Dynamic api'
)
})

expect((await browser.elementsByCss('title')).length).toBe(1)
})

it('should only insert metadata once for parallel routes when there is a missing slot', async () => {
const browser = await next.browser('/parallel-routes')
await browser.elementByCss('[href="/parallel-routes/no-bar"]').click()

await retry(async () => {
expect(await browser.elementByCss('title').text()).toContain(
'Dynamic api'
)
})

expect((await browser.elementsByCss('title')).length).toBe(1)
})

describe('dynamic api', () => {
Expand Down
Loading