Skip to content

[metadata] new metadata insertion API and support PPR #75366

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 14 commits into from
Feb 8, 2025
Original file line number Diff line number Diff line change
Expand Up @@ -797,9 +797,6 @@ function finishPendingCacheNode(
// a pending promise that needs to be resolved with the dynamic head from
// the server.
const head = cacheNode.head
// TODO: change head back to ReactNode when metadata
// is stably rendered in body
// Handle head[0] - viewport
if (isDeferredRsc(head)) {
head.resolve(dynamicHead)
}
Expand Down
1 change: 1 addition & 0 deletions packages/next/src/export/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ export interface ExportPageInput {
nextConfigOutput?: NextConfigComplete['output']
enableExperimentalReact?: boolean
sriEnabled: boolean
streamingMetadata: boolean | undefined
}

export type ExportRouteResult =
Expand Down
9 changes: 9 additions & 0 deletions packages/next/src/export/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,13 @@ async function exportPageImpl(
)
}

// During the export phase in next build, if it's using PPR we can serve streaming metadata
// when it's available. When we're building the PPR rendering result, we don't need to rely
// on the user agent. The result can be determined to serve streaming on infrastructure level.
const serveStreamingMetadata = Boolean(
isRoutePPREnabled && input.streamingMetadata
)

const renderOpts: WorkerRenderOpts = {
...components,
...input.renderOpts,
Expand All @@ -262,6 +269,7 @@ async function exportPageImpl(
disableOptimizedLoading,
locale,
supportsDynamicResponse: false,
serveStreamingMetadata,
experimental: {
...input.renderOpts.experimental,
isRoutePPREnabled,
Expand Down Expand Up @@ -416,6 +424,7 @@ export async function exportPages(
debugOutput: options.debugOutput,
enableExperimentalReact: needsExperimentalReact(nextConfig),
sriEnabled: Boolean(nextConfig.experimental.sri?.algorithm),
streamingMetadata: nextConfig.experimental.streamingMetadata,
buildId: input.buildId,
}),
// If exporting the page takes longer than the timeout, reject the promise.
Expand Down
22 changes: 7 additions & 15 deletions packages/next/src/lib/metadata/async-metadata.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,13 @@
'use client'

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

// We need to wait for metadata on server once it's resolved, and insert into
// the HTML through `useServerInsertedHTML`. It will suspense in <head> during SSR.
function ServerInsertMetadata({ promise }: { promise: Promise<any> }) {
let metadataToFlush: React.ReactNode = use(promise)

useServerInsertedHTML(() => {
if (metadataToFlush) {
const flushing = metadataToFlush
// reset to null to ensure we only flush it once
metadataToFlush = null
return flushing
}
})
function ServerInsertMetadata({ promise }: { promise: Promise<JSX.Element> }) {
// Apply use() to the metadata promise to suspend the rendering in SSR.
const metadata = use(promise)
// Insert metadata into the HTML stream through the `useServerInsertedMetadata`
useServerInsertedMetadata(() => metadata)

return null
}
Expand Down
29 changes: 23 additions & 6 deletions packages/next/src/lib/metadata/metadata.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { GetDynamicParamFromSegment } from '../../server/app-render/app-ren
import type { LoaderTree } from '../../server/lib/app-dir-module'
import type { CreateServerParamsForMetadata } from '../../server/request/params'

import { cache, cloneElement } from 'react'
import { Suspense, cache, cloneElement } from 'react'
import {
AppleWebAppMeta,
FormatDetectionMeta,
Expand Down Expand Up @@ -38,6 +38,7 @@ import {
VIEWPORT_BOUNDARY_NAME,
} from './metadata-constants'
import { AsyncMetadata } from './async-metadata'
import { isPostpone } from '../../server/lib/router-utils/is-postpone'

// Use a promise to share the status of the metadata resolving,
// returning two components `MetadataTree` and `MetadataOutlet`
Expand Down Expand Up @@ -147,8 +148,8 @@ export function createMetadataComponents({
async function resolveFinalMetadata() {
try {
return await metadata()
} catch (error) {
if (!errorType && isHTTPAccessFallbackError(error)) {
} catch (metadataErr) {
if (!errorType && isHTTPAccessFallbackError(metadataErr)) {
try {
return await getNotFoundMetadata(
tree,
Expand All @@ -158,7 +159,18 @@ export function createMetadataComponents({
createServerParamsForMetadata,
workStore
)
} catch {}
} catch (notFoundMetadataErr) {
// In PPR rendering we still need to throw the postpone error.
// If metadata is postponed, React needs to be aware of the location of error.
if (isPostpone(notFoundMetadataErr)) {
throw notFoundMetadataErr
}
}
}
// In PPR rendering we still need to throw the postpone error.
// If metadata is postponed, React needs to be aware of the location of error.
if (isPostpone(metadataErr)) {
throw metadataErr
}
// We don't actually want to error in this component. We will
// also error in the MetadataOutlet which causes the error to
Expand All @@ -168,10 +180,15 @@ export function createMetadataComponents({
}
}
async function Metadata() {
const promise = resolveFinalMetadata()
if (serveStreamingMetadata) {
return <AsyncMetadata promise={resolveFinalMetadata()} />
return (
<Suspense fallback={null}>
<AsyncMetadata promise={promise} />
</Suspense>
)
}
return await resolveFinalMetadata()
return await promise
}

Metadata.displayName = METADATA_BOUNDARY_NAME
Expand Down
Loading
Loading