diff --git a/packages/next/src/lib/metadata/async-metadata.tsx b/packages/next/src/lib/metadata/async-metadata.tsx index f97287e55efe6..91141e7c5e47c 100644 --- a/packages/next/src/lib/metadata/async-metadata.tsx +++ b/packages/next/src/lib/metadata/async-metadata.tsx @@ -1,22 +1,41 @@ 'use client' -import { use, type JSX } from 'react' +import { Suspense, use } from 'react' import { useServerInsertedMetadata } from '../../server/app-render/metadata-insertion/use-server-inserted-metadata' -function ServerInsertMetadata({ promise }: { promise: Promise }) { +export type StreamingMetadataResolvedState = { + metadata: React.ReactNode + error: unknown | null + digest: string | undefined +} + +function ServerInsertMetadata({ + promise, +}: { + promise: Promise +}) { // Apply use() to the metadata promise to suspend the rendering in SSR. - const metadata = use(promise) + const { metadata } = use(promise) // Insert metadata into the HTML stream through the `useServerInsertedMetadata` useServerInsertedMetadata(() => metadata) return null } -function BrowserResolvedMetadata({ promise }: { promise: Promise }) { - return use(promise) +function BrowserResolvedMetadata({ + promise, +}: { + promise: Promise +}) { + const { metadata } = use(promise) + return metadata } -export function AsyncMetadata({ promise }: { promise: Promise }) { +export function AsyncMetadata({ + promise, +}: { + promise: Promise +}) { return ( <> {typeof window === 'undefined' ? ( @@ -27,3 +46,32 @@ export function AsyncMetadata({ promise }: { promise: Promise }) { ) } + +function MetadataOutlet({ + promise, +}: { + promise: Promise +}) { + const { error, digest } = use(promise) + if (error) { + if (digest) { + // The error will lose its original digest after passing from server layer to client layer; + // We recover the digest property here to override the React created one if original digest exists. + ;(error as any).digest = digest + } + throw error + } + return null +} + +export function AsyncMetadataOutlet({ + promise, +}: { + promise: Promise +}) { + return ( + + + + ) +} diff --git a/packages/next/src/lib/metadata/metadata.tsx b/packages/next/src/lib/metadata/metadata.tsx index 96d847179c3b0..8241954d394d2 100644 --- a/packages/next/src/lib/metadata/metadata.tsx +++ b/packages/next/src/lib/metadata/metadata.tsx @@ -1,9 +1,9 @@ +import React, { Suspense, cache, cloneElement } from 'react' import type { ParsedUrlQuery } from 'querystring' import type { GetDynamicParamFromSegment } from '../../server/app-render/app-render' import type { LoaderTree } from '../../server/lib/app-dir-module' import type { CreateServerParamsForMetadata } from '../../server/request/params' - -import { Suspense, cache, cloneElement } from 'react' +import type { StreamingMetadataResolvedState } from './async-metadata' import { AppleWebAppMeta, FormatDetectionMeta, @@ -37,7 +37,7 @@ import { METADATA_BOUNDARY_NAME, VIEWPORT_BOUNDARY_NAME, } from './metadata-constants' -import { AsyncMetadata } from './async-metadata' +import { AsyncMetadata, AsyncMetadataOutlet } from './async-metadata' import { isPostpone } from '../../server/lib/router-utils/is-postpone' // Use a promise to share the status of the metadata resolving, @@ -75,6 +75,7 @@ export function createMetadataComponents({ ViewportTree: React.ComponentType getMetadataReady: () => Promise getViewportReady: () => Promise + StreamingMetadataOutlet: React.ComponentType } { function ViewportTree() { return ( @@ -145,13 +146,21 @@ export function createMetadataComponents({ ) } - async function resolveFinalMetadata() { + async function resolveFinalMetadata(): Promise { + let result: React.ReactNode + let error = null try { - return await metadata() + result = await metadata() + return { + metadata: result, + error: null, + digest: undefined, + } } catch (metadataErr) { + error = metadataErr if (!errorType && isHTTPAccessFallbackError(metadataErr)) { try { - return await getNotFoundMetadata( + result = await getNotFoundMetadata( tree, searchParams, getDynamicParamFromSegment, @@ -159,7 +168,13 @@ export function createMetadataComponents({ createServerParamsForMetadata, workStore ) + return { + metadata: result, + error, + digest: (error as any)?.digest, + } } catch (notFoundMetadataErr) { + error = 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 (serveStreamingMetadata && isPostpone(notFoundMetadataErr)) { @@ -176,7 +191,11 @@ export function createMetadataComponents({ // also error in the MetadataOutlet which causes the error to // bubble from the right position in the page to be caught by the // appropriate boundaries - return null + return { + metadata: result, + error, + digest: (error as any)?.digest, + } } } async function Metadata() { @@ -188,7 +207,8 @@ export function createMetadataComponents({ ) } - return await promise + const metadataState = await promise + return metadataState.metadata } Metadata.displayName = METADATA_BOUNDARY_NAME @@ -207,11 +227,19 @@ export function createMetadataComponents({ return undefined } + function StreamingMetadataOutlet() { + if (serveStreamingMetadata) { + return + } + return null + } + return { ViewportTree, MetadataTree, getViewportReady, getMetadataReady, + StreamingMetadataOutlet, } } diff --git a/packages/next/src/server/app-render/app-render.tsx b/packages/next/src/server/app-render/app-render.tsx index 8a55b2cfe01ca..f4e9394e6f65c 100644 --- a/packages/next/src/server/app-render/app-render.tsx +++ b/packages/next/src/server/app-render/app-render.tsx @@ -321,14 +321,13 @@ function createDivergedMetadataComponents( serveStreamingMetadata: boolean ): { StaticMetadata: React.ComponentType<{}> - StreamingMetadata: React.ComponentType<{}> + StreamingMetadata: React.ComponentType<{}> | null } { function EmptyMetadata() { return null } - const StreamingMetadata: React.ComponentType<{}> = serveStreamingMetadata - ? Metadata - : EmptyMetadata + const StreamingMetadata: React.ComponentType<{}> | null = + serveStreamingMetadata ? Metadata : null const StaticMetadata: React.ComponentType<{}> = serveStreamingMetadata ? EmptyMetadata @@ -490,23 +489,28 @@ async function generateDynamicRSCPayload( const preloadCallbacks: PreloadCallbacks = [] const searchParams = createServerSearchParamsForMetadata(query, workStore) - const { ViewportTree, MetadataTree, getViewportReady, getMetadataReady } = - createMetadataComponents({ - tree: loaderTree, - searchParams, - metadataContext: createTrackedMetadataContext( - url.pathname, - ctx.renderOpts, - workStore - ), - getDynamicParamFromSegment, - appUsingSizeAdjustment, - createServerParamsForMetadata, - workStore, - MetadataBoundary, - ViewportBoundary, - serveStreamingMetadata, - }) + const { + ViewportTree, + MetadataTree, + getViewportReady, + getMetadataReady, + StreamingMetadataOutlet, + } = createMetadataComponents({ + tree: loaderTree, + searchParams, + metadataContext: createTrackedMetadataContext( + url.pathname, + ctx.renderOpts, + workStore + ), + getDynamicParamFromSegment, + appUsingSizeAdjustment, + createServerParamsForMetadata, + workStore, + MetadataBoundary, + ViewportBoundary, + serveStreamingMetadata, + }) const { StreamingMetadata, StaticMetadata } = createDivergedMetadataComponents(() => { @@ -540,6 +544,7 @@ async function generateDynamicRSCPayload( getMetadataReady, preloadCallbacks, StreamingMetadata, + StreamingMetadataOutlet, }) ).map((path) => path.slice(1)) // remove the '' (root) segment } @@ -801,24 +806,29 @@ async function getRSCPayload( const serveStreamingMetadata = getServeStreamingMetadata(ctx) const searchParams = createServerSearchParamsForMetadata(query, workStore) - const { ViewportTree, MetadataTree, getViewportReady, getMetadataReady } = - createMetadataComponents({ - tree, - errorType: is404 ? 'not-found' : undefined, - searchParams, - metadataContext: createTrackedMetadataContext( - url.pathname, - ctx.renderOpts, - workStore - ), - getDynamicParamFromSegment, - appUsingSizeAdjustment, - createServerParamsForMetadata, - workStore, - MetadataBoundary, - ViewportBoundary, - serveStreamingMetadata: serveStreamingMetadata, - }) + const { + ViewportTree, + MetadataTree, + getViewportReady, + getMetadataReady, + StreamingMetadataOutlet, + } = createMetadataComponents({ + tree, + errorType: is404 ? 'not-found' : undefined, + searchParams, + metadataContext: createTrackedMetadataContext( + url.pathname, + ctx.renderOpts, + workStore + ), + getDynamicParamFromSegment, + appUsingSizeAdjustment, + createServerParamsForMetadata, + workStore, + MetadataBoundary, + ViewportBoundary, + serveStreamingMetadata, + }) const preloadCallbacks: PreloadCallbacks = [] @@ -844,6 +854,7 @@ async function getRSCPayload( preloadCallbacks, authInterrupts: ctx.renderOpts.experimental.authInterrupts, StreamingMetadata, + StreamingMetadataOutlet, }) // When the `vary` response header is present with `Next-URL`, that means there's a chance @@ -987,9 +998,7 @@ async function getErrorRSCPayload( const seedData: CacheNodeSeedData = [ initialTree[0], - - - + {StreamingMetadata ? : null} {process.env.NODE_ENV !== 'production' && err ? (