Skip to content

Commit bc019cb

Browse files
huozhipull[bot]
authored andcommitted
[metadata] Move metadata rendering adjacent to page component (#74262)
1 parent 276f4d3 commit bc019cb

File tree

6 files changed

+98
-64
lines changed

6 files changed

+98
-64
lines changed

packages/next/src/client/components/router-reducer/ppr-navigations.ts

+6-5
Original file line numberDiff line numberDiff line change
@@ -700,8 +700,10 @@ function createPendingCacheNode(
700700
rsc: createDeferredRsc() as React.ReactNode,
701701
head: isLeafSegment
702702
? [
703+
// TODO: change head back to ReactNode when metadata
704+
// is stably rendered in body
703705
createDeferredRsc() as React.ReactNode,
704-
createDeferredRsc() as React.ReactNode,
706+
null,
705707
]
706708
: [null, null],
707709
}
@@ -805,13 +807,12 @@ function finishPendingCacheNode(
805807
// a pending promise that needs to be resolved with the dynamic head from
806808
// the server.
807809
const head = cacheNode.head
808-
// Handle head[0] - viewport and head[1] - metadata
810+
// TODO: change head back to ReactNode when metadata
811+
// is stably rendered in body
812+
// Handle head[0] - viewport
809813
if (isDeferredRsc(head[0])) {
810814
head[0].resolve(dynamicHead[0])
811815
}
812-
if (isDeferredRsc(head[1])) {
813-
head[1].resolve(dynamicHead[1])
814-
}
815816
}
816817

817818
export function abortTask(task: Task, error: any): void {

packages/next/src/lib/metadata/generate/basic.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ function resolveViewportLayout(viewport: Viewport) {
3131

3232
export function ViewportMeta({ viewport }: { viewport: ResolvedViewport }) {
3333
return MetaFilter([
34+
<meta charSet="utf-8" />,
3435
Meta({ name: 'viewport', content: resolveViewportLayout(viewport) }),
3536
...(viewport.themeColor
3637
? viewport.themeColor.map((themeColor) =>
@@ -51,7 +52,6 @@ export function BasicMeta({ metadata }: { metadata: ResolvedMetadata }) {
5152
: undefined
5253

5354
return MetaFilter([
54-
<meta charSet="utf-8" />,
5555
metadata.title !== null && metadata.title.absolute ? (
5656
<title>{metadata.title.absolute}</title>
5757
) : null,

packages/next/src/server/app-render/app-render.tsx

+29-17
Original file line numberDiff line numberDiff line change
@@ -467,6 +467,16 @@ async function generateDynamicRSCPayload(
467467
MetadataBoundary,
468468
ViewportBoundary,
469469
})
470+
471+
const MetadataComponent = () => {
472+
return (
473+
<React.Fragment key={flightDataPathMetadataKey}>
474+
{/* Adding requestId as react key to make metadata remount for each render */}
475+
<MetadataTree key={requestId} />
476+
</React.Fragment>
477+
)
478+
}
479+
470480
flightData = (
471481
await walkTreeWithFlightRouterState({
472482
ctx,
@@ -481,10 +491,7 @@ async function generateDynamicRSCPayload(
481491
{/* Adding requestId as react key to make metadata remount for each render */}
482492
<ViewportTree key={requestId} />
483493
</React.Fragment>,
484-
<React.Fragment key={flightDataPathMetadataKey}>
485-
{/* Adding requestId as react key to make metadata remount for each render */}
486-
<MetadataTree key={requestId} />
487-
</React.Fragment>,
494+
null,
488495
],
489496
injectedCSS: new Set(),
490497
injectedJS: new Set(),
@@ -493,6 +500,7 @@ async function generateDynamicRSCPayload(
493500
getViewportReady,
494501
getMetadataReady,
495502
preloadCallbacks,
503+
MetadataComponent,
496504
})
497505
).map((path) => path.slice(1)) // remove the '' (root) segment
498506
}
@@ -766,6 +774,15 @@ async function getRSCPayload(
766774

767775
const preloadCallbacks: PreloadCallbacks = []
768776

777+
function MetadataComponent() {
778+
return (
779+
<React.Fragment key={flightDataPathMetadataKey}>
780+
{/* Not add requestId as react key to ensure segment prefetch could result consistently if nothing changed */}
781+
<MetadataTree />
782+
</React.Fragment>
783+
)
784+
}
785+
769786
const seedData = await createComponentTree({
770787
ctx,
771788
loaderTree: tree,
@@ -779,6 +796,7 @@ async function getRSCPayload(
779796
missingSlots,
780797
preloadCallbacks,
781798
authInterrupts: ctx.renderOpts.experimental.authInterrupts,
799+
MetadataComponent,
782800
})
783801

784802
// When the `vary` response header is present with `Next-URL`, that means there's a chance
@@ -788,13 +806,6 @@ async function getRSCPayload(
788806
const couldBeIntercepted =
789807
typeof varyHeader === 'string' && varyHeader.includes(NEXT_URL)
790808

791-
const initialHeadMetadata = (
792-
<React.Fragment key={flightDataPathMetadataKey}>
793-
{/* Adding requestId as react key to make metadata remount for each render */}
794-
<MetadataTree key={ctx.requestId} />
795-
</React.Fragment>
796-
)
797-
798809
const initialHeadViewport = (
799810
<React.Fragment key={flightDataPathViewportKey}>
800811
<NonIndex ctx={ctx} />
@@ -825,7 +836,7 @@ async function getRSCPayload(
825836
[
826837
initialTree,
827838
seedData,
828-
[initialHeadViewport, initialHeadMetadata],
839+
[initialHeadViewport, null],
829840
isPossiblyPartialHead,
830841
] as FlightDataPath,
831842
],
@@ -892,6 +903,7 @@ async function getErrorRSCPayload(
892903
<MetadataTree key={requestId} />
893904
</React.Fragment>
894905
)
906+
895907
const initialHeadViewport = (
896908
<React.Fragment key={flightDataPathViewportKey}>
897909
<NonIndex ctx={ctx} />
@@ -911,11 +923,11 @@ async function getErrorRSCPayload(
911923

912924
// For metadata notFound error there's no global not found boundary on top
913925
// so we create a not found page with AppRouter
914-
const initialSeedData: CacheNodeSeedData = [
926+
const seedData: CacheNodeSeedData = [
915927
initialTree[0],
916928
<html id="__next_error__">
917-
<head></head>
918-
<body></body>
929+
<head>{initialHeadMetadata}</head>
930+
<body />
919931
</html>,
920932
{},
921933
null,
@@ -937,8 +949,8 @@ async function getErrorRSCPayload(
937949
f: [
938950
[
939951
initialTree,
940-
initialSeedData,
941-
[initialHeadViewport, initialHeadMetadata],
952+
seedData,
953+
[initialHeadViewport, null],
942954
isPossiblyPartialHead,
943955
] as FlightDataPath,
944956
],

packages/next/src/server/app-render/create-component-tree.tsx

+37-23
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import type { LoadingModuleData } from '../../shared/lib/app-router-context.shar
1818
import type { Params } from '../request/params'
1919
import { workUnitAsyncStorage } from './work-unit-async-storage.external'
2020
import { OUTLET_BOUNDARY_NAME } from '../../lib/metadata/metadata-constants'
21+
import { DEFAULT_SEGMENT_KEY } from '../../shared/lib/segment'
2122

2223
/**
2324
* Use the provided loader tree to create the React Component tree.
@@ -35,6 +36,7 @@ export function createComponentTree(props: {
3536
missingSlots?: Set<string>
3637
preloadCallbacks: PreloadCallbacks
3738
authInterrupts: boolean
39+
MetadataComponent: React.ComponentType<{}>
3840
}): Promise<CacheNodeSeedData> {
3941
return getTracer().trace(
4042
NextNodeServerSpan.createComponentTree,
@@ -70,6 +72,7 @@ async function createComponentTreeInternal({
7072
missingSlots,
7173
preloadCallbacks,
7274
authInterrupts,
75+
MetadataComponent,
7376
}: {
7477
loaderTree: LoaderTree
7578
parentParams: Params
@@ -83,6 +86,7 @@ async function createComponentTreeInternal({
8386
missingSlots?: Set<string>
8487
preloadCallbacks: PreloadCallbacks
8588
authInterrupts: boolean
89+
MetadataComponent: React.ComponentType<{}>
8690
}): Promise<CacheNodeSeedData> {
8791
const {
8892
renderOpts: { nextConfigOutput, experimental },
@@ -221,27 +225,6 @@ async function createComponentTreeInternal({
221225
})
222226
: []
223227

224-
const notFoundElement = NotFound ? (
225-
<>
226-
{notFoundStyles}
227-
<NotFound />
228-
</>
229-
) : undefined
230-
231-
const forbiddenElement = Forbidden ? (
232-
<>
233-
{forbiddenStyles}
234-
<Forbidden />
235-
</>
236-
) : undefined
237-
238-
const unauthorizedElement = Unauthorized ? (
239-
<>
240-
{unauthorizedStyles}
241-
<Unauthorized />
242-
</>
243-
) : undefined
244-
245228
let dynamic = layoutOrPageMod?.dynamic
246229

247230
if (nextConfigOutput === 'export') {
@@ -409,7 +392,35 @@ async function createComponentTreeInternal({
409392
workStore.rootParams = currentParams
410393
}
411394

412-
//
395+
// Only render metadata on the actual SSR'd segment not the `default` segment,
396+
// as it's used as a placeholder for navigation.
397+
const metadata =
398+
actualSegment !== DEFAULT_SEGMENT_KEY ? <MetadataComponent /> : undefined
399+
400+
const notFoundElement = NotFound ? (
401+
<>
402+
{metadata}
403+
{notFoundStyles}
404+
<NotFound />
405+
</>
406+
) : undefined
407+
408+
const forbiddenElement = Forbidden ? (
409+
<>
410+
{metadata}
411+
{forbiddenStyles}
412+
<Forbidden />
413+
</>
414+
) : undefined
415+
416+
const unauthorizedElement = Unauthorized ? (
417+
<>
418+
{metadata}
419+
{unauthorizedStyles}
420+
<Unauthorized />
421+
</>
422+
) : undefined
423+
413424
// TODO: Combine this `map` traversal with the loop below that turns the array
414425
// into an object.
415426
const parallelRouteMap = await Promise.all(
@@ -504,7 +515,8 @@ async function createComponentTreeInternal({
504515
ctx,
505516
missingSlots,
506517
preloadCallbacks,
507-
authInterrupts: authInterrupts,
518+
authInterrupts,
519+
MetadataComponent,
508520
})
509521

510522
childCacheNodeSeedData = seedData
@@ -657,6 +669,7 @@ async function createComponentTreeInternal({
657669
return [
658670
actualSegment,
659671
<React.Fragment key={cacheNodeKey}>
672+
{metadata}
660673
{pageElement}
661674
{layerAssets}
662675
<OutletBoundary>
@@ -792,6 +805,7 @@ async function createComponentTreeInternal({
792805
notFound={
793806
NotFound ? (
794807
<>
808+
{metadata}
795809
{layerAssets}
796810
<SegmentComponent params={params}>
797811
{notFoundStyles}

packages/next/src/server/app-render/walk-tree-with-flight-router-state.tsx

+4
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export async function walkTreeWithFlightRouterState({
4040
getMetadataReady,
4141
ctx,
4242
preloadCallbacks,
43+
MetadataComponent,
4344
}: {
4445
loaderTreeToFilter: LoaderTree
4546
parentParams: { [key: string]: string | string[] }
@@ -54,6 +55,7 @@ export async function walkTreeWithFlightRouterState({
5455
getViewportReady: () => Promise<void>
5556
ctx: AppRenderContext
5657
preloadCallbacks: PreloadCallbacks
58+
MetadataComponent: React.ComponentType<{}>
5759
}): Promise<FlightDataPath[]> {
5860
const {
5961
renderOpts: { nextFontManifest, experimental },
@@ -202,6 +204,7 @@ export async function walkTreeWithFlightRouterState({
202204
getMetadataReady,
203205
preloadCallbacks,
204206
authInterrupts: experimental.authInterrupts,
207+
MetadataComponent,
205208
}
206209
)
207210

@@ -261,6 +264,7 @@ export async function walkTreeWithFlightRouterState({
261264
getViewportReady,
262265
getMetadataReady,
263266
preloadCallbacks,
267+
MetadataComponent,
264268
})
265269

266270
for (const subPath of subPaths) {

test/development/acceptance-app/hydration-error.test.ts

+21-18
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,7 @@ describe('Error overlay for hydration errors in App router', () => {
439439
<RedirectBoundary>
440440
<RedirectErrorBoundary router={{...}}>
441441
<InnerLayoutRouter url="/" tree={[...]} cacheNode={{lazyData:null, ...}} segmentPath={[...]}>
442+
<__next_metadata_boundary__>
442443
<ClientPageRoot Component={function Page} searchParams={{}} params={{}}>
443444
<Page params={Promise} searchParams={Promise}>
444445
> <table>
@@ -894,18 +895,19 @@ describe('Error overlay for hydration errors in App router', () => {
894895
<RedirectBoundary>
895896
<RedirectErrorBoundary router={{...}}>
896897
<InnerLayoutRouter url="/" tree={[...]} cacheNode={{lazyData:null, ...}} segmentPath={[...]}>
897-
<ClientPageRoot Component={function Page} searchParams={{}} params={{}}>
898-
<Page params={Promise} searchParams={Promise}>
899-
<div>
898+
<__next_metadata_boundary__>
899+
<ClientPageRoot Component={function Page} searchParams={{}} params={{}}>
900+
<Page params={Promise} searchParams={Promise}>
900901
<div>
901902
<div>
902903
<div>
903-
<Mismatch>
904-
<p>
905-
<span>
906-
...
907-
+ client
908-
- server"
904+
<div>
905+
<Mismatch>
906+
<p>
907+
<span>
908+
...
909+
+ client
910+
- server"
909911
`)
910912
} else {
911913
expect(fullPseudoHtml).toMatchInlineSnapshot(`
@@ -914,18 +916,19 @@ describe('Error overlay for hydration errors in App router', () => {
914916
<RedirectBoundary>
915917
<RedirectErrorBoundary router={{...}}>
916918
<InnerLayoutRouter url="/" tree={[...]} cacheNode={{lazyData:null, ...}} segmentPath={[...]}>
917-
<ClientPageRoot Component={function Page} searchParams={{}} params={{}}>
918-
<Page params={Promise} searchParams={Promise}>
919-
<div>
919+
<__next_metadata_boundary__>
920+
<ClientPageRoot Component={function Page} searchParams={{}} params={{}}>
921+
<Page params={Promise} searchParams={Promise}>
920922
<div>
921923
<div>
922924
<div>
923-
<Mismatch>
924-
<p>
925-
<span>
926-
...
927-
+ client
928-
- server"
925+
<div>
926+
<Mismatch>
927+
<p>
928+
<span>
929+
...
930+
+ client
931+
- server"
929932
`)
930933
}
931934
})

0 commit comments

Comments
 (0)