Skip to content

Commit 873b8d5

Browse files
committed
[metadata] internal api useServerInsertedMetadata for inserting suspended metadata
append to ppr body add ppr test add new test case change the insertion stream helper fix the hidden postpone error causing dynamic missing metadata revert test suite changes
1 parent c70bf42 commit 873b8d5

File tree

28 files changed

+508
-81
lines changed

28 files changed

+508
-81
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
@@ -61,6 +61,7 @@ export interface ExportPageInput {
6161
nextConfigOutput?: NextConfigComplete['output']
6262
enableExperimentalReact?: boolean
6363
sriEnabled: boolean
64+
streamingMetadata: boolean | undefined
6465
}
6566

6667
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

+14-3
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`
@@ -160,6 +161,11 @@ export function createMetadataComponents({
160161
)
161162
} catch {}
162163
}
164+
// In PPR rendering we still need to throw the postpone error.
165+
// If metadata is postponed, React needs to be aware of the location of error.
166+
if (isPostpone(error)) {
167+
throw error
168+
}
163169
// We don't actually want to error in this component. We will
164170
// also error in the MetadataOutlet which causes the error to
165171
// bubble from the right position in the page to be caught by the
@@ -168,10 +174,15 @@ export function createMetadataComponents({
168174
}
169175
}
170176
async function Metadata() {
177+
const promise = resolveFinalMetadata()
171178
if (serveStreamingMetadata) {
172-
return <AsyncMetadata promise={resolveFinalMetadata()} />
179+
return (
180+
<Suspense fallback={null}>
181+
<AsyncMetadata promise={promise} />
182+
</Suspense>
183+
)
173184
}
174-
return await resolveFinalMetadata()
185+
return await promise
175186
}
176187

177188
Metadata.displayName = METADATA_BOUNDARY_NAME

0 commit comments

Comments
 (0)