Skip to content

Commit 2220d73

Browse files
authored
[metadata] new metadata insertion API and support PPR (#75366)
1 parent d27cdc0 commit 2220d73

File tree

33 files changed

+668
-92
lines changed

33 files changed

+668
-92
lines changed

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

-3
Original file line numberDiff line numberDiff line change
@@ -797,9 +797,6 @@ function finishPendingCacheNode(
797797
// a pending promise that needs to be resolved with the dynamic head from
798798
// the server.
799799
const head = cacheNode.head
800-
// TODO: change head back to ReactNode when metadata
801-
// is stably rendered in body
802-
// Handle head[0] - viewport
803800
if (isDeferredRsc(head)) {
804801
head.resolve(dynamicHead)
805802
}

Diff for: packages/next/src/export/types.ts

+1
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ export interface ExportPageInput {
6262
nextConfigOutput?: NextConfigComplete['output']
6363
enableExperimentalReact?: boolean
6464
sriEnabled: boolean
65+
streamingMetadata: boolean | undefined
6566
}
6667

6768
export type ExportRouteResult =

Diff for: packages/next/src/export/worker.ts

+9
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,13 @@ async function exportPageImpl(
253253
)
254254
}
255255

256+
// During the export phase in next build, if it's using PPR we can serve streaming metadata
257+
// when it's available. When we're building the PPR rendering result, we don't need to rely
258+
// on the user agent. The result can be determined to serve streaming on infrastructure level.
259+
const serveStreamingMetadata = Boolean(
260+
isRoutePPREnabled && input.streamingMetadata
261+
)
262+
256263
const renderOpts: WorkerRenderOpts = {
257264
...components,
258265
...input.renderOpts,
@@ -262,6 +269,7 @@ async function exportPageImpl(
262269
disableOptimizedLoading,
263270
locale,
264271
supportsDynamicResponse: false,
272+
serveStreamingMetadata,
265273
experimental: {
266274
...input.renderOpts.experimental,
267275
isRoutePPREnabled,
@@ -416,6 +424,7 @@ export async function exportPages(
416424
debugOutput: options.debugOutput,
417425
enableExperimentalReact: needsExperimentalReact(nextConfig),
418426
sriEnabled: Boolean(nextConfig.experimental.sri?.algorithm),
427+
streamingMetadata: nextConfig.experimental.streamingMetadata,
419428
buildId: input.buildId,
420429
}),
421430
// If exporting the page takes longer than the timeout, reject the promise.

Diff for: packages/next/src/lib/metadata/async-metadata.tsx

+7-15
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,13 @@
11
'use client'
22

3-
import { use } from 'react'
4-
import { useServerInsertedHTML } from '../../client/components/navigation'
3+
import { use, type JSX } from 'react'
4+
import { useServerInsertedMetadata } from '../../server/app-render/metadata-insertion/use-server-inserted-metadata'
55

6-
// We need to wait for metadata on server once it's resolved, and insert into
7-
// the HTML through `useServerInsertedHTML`. It will suspense in <head> during SSR.
8-
function ServerInsertMetadata({ promise }: { promise: Promise<any> }) {
9-
let metadataToFlush: React.ReactNode = use(promise)
10-
11-
useServerInsertedHTML(() => {
12-
if (metadataToFlush) {
13-
const flushing = metadataToFlush
14-
// reset to null to ensure we only flush it once
15-
metadataToFlush = null
16-
return flushing
17-
}
18-
})
6+
function ServerInsertMetadata({ promise }: { promise: Promise<JSX.Element> }) {
7+
// Apply use() to the metadata promise to suspend the rendering in SSR.
8+
const metadata = use(promise)
9+
// Insert metadata into the HTML stream through the `useServerInsertedMetadata`
10+
useServerInsertedMetadata(() => metadata)
1911

2012
return null
2113
}

Diff for: packages/next/src/lib/metadata/metadata.tsx

+23-6
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { GetDynamicParamFromSegment } from '../../server/app-render/app-ren
33
import type { LoaderTree } from '../../server/lib/app-dir-module'
44
import type { CreateServerParamsForMetadata } from '../../server/request/params'
55

6-
import { cache, cloneElement } from 'react'
6+
import { Suspense, cache, cloneElement } from 'react'
77
import {
88
AppleWebAppMeta,
99
FormatDetectionMeta,
@@ -38,6 +38,7 @@ import {
3838
VIEWPORT_BOUNDARY_NAME,
3939
} from './metadata-constants'
4040
import { AsyncMetadata } from './async-metadata'
41+
import { isPostpone } from '../../server/lib/router-utils/is-postpone'
4142

4243
// Use a promise to share the status of the metadata resolving,
4344
// returning two components `MetadataTree` and `MetadataOutlet`
@@ -147,8 +148,8 @@ export function createMetadataComponents({
147148
async function resolveFinalMetadata() {
148149
try {
149150
return await metadata()
150-
} catch (error) {
151-
if (!errorType && isHTTPAccessFallbackError(error)) {
151+
} catch (metadataErr) {
152+
if (!errorType && isHTTPAccessFallbackError(metadataErr)) {
152153
try {
153154
return await getNotFoundMetadata(
154155
tree,
@@ -158,7 +159,18 @@ export function createMetadataComponents({
158159
createServerParamsForMetadata,
159160
workStore
160161
)
161-
} catch {}
162+
} catch (notFoundMetadataErr) {
163+
// In PPR rendering we still need to throw the postpone error.
164+
// If metadata is postponed, React needs to be aware of the location of error.
165+
if (isPostpone(notFoundMetadataErr)) {
166+
throw notFoundMetadataErr
167+
}
168+
}
169+
}
170+
// In PPR rendering we still need to throw the postpone error.
171+
// If metadata is postponed, React needs to be aware of the location of error.
172+
if (isPostpone(metadataErr)) {
173+
throw metadataErr
162174
}
163175
// We don't actually want to error in this component. We will
164176
// also error in the MetadataOutlet which causes the error to
@@ -168,10 +180,15 @@ export function createMetadataComponents({
168180
}
169181
}
170182
async function Metadata() {
183+
const promise = resolveFinalMetadata()
171184
if (serveStreamingMetadata) {
172-
return <AsyncMetadata promise={resolveFinalMetadata()} />
185+
return (
186+
<Suspense fallback={null}>
187+
<AsyncMetadata promise={promise} />
188+
</Suspense>
189+
)
173190
}
174-
return await resolveFinalMetadata()
191+
return await promise
175192
}
176193

177194
Metadata.displayName = METADATA_BOUNDARY_NAME

0 commit comments

Comments
 (0)