Repository: vercel/next.js
Pull request: [metadata] only render one metadata outlet

### What

When streaming metadata rendered, we're still having two metadata outlets rendered, which the 1st blocking metadata outlet will take precedence.

This is help to support dynmiacIO case where we need those components being wrapped under Suspense boundaries. 

Changed files:
filename: packages/next/src/lib/metadata/metadata.tsx
patch: @@ -78,7 +78,7 @@ export function createMetadataComponents({
   ViewportTree: React.ComponentType
   getMetadataReady: () => Promise<void>
   getViewportReady: () => Promise<void>
-  StreamingMetadataOutlet: React.ComponentType
+  StreamingMetadataOutlet: React.ComponentType | null
 } {
   const searchParams = createServerSearchParamsForMetadata(
     parsedQuery,
@@ -243,13 +243,14 @@ export function createMetadataComponents({
     return undefined
   }
 
-  function StreamingMetadataOutlet() {
-    if (serveStreamingMetadata) {
-      return <AsyncMetadataOutlet promise={resolveFinalMetadata()} />
-    }
-    return null
+  function StreamingMetadataOutletImpl() {
+    return <AsyncMetadataOutlet promise={resolveFinalMetadata()} />
   }
 
+  const StreamingMetadataOutlet = serveStreamingMetadata
+    ? StreamingMetadataOutletImpl
+    : null
+
   return {
     ViewportTree,
     MetadataTree,

filename: packages/next/src/server/app-render/create-component-tree.tsx
patch: @@ -42,7 +42,7 @@ export function createComponentTree(props: {
   missingSlots?: Set<string>
   preloadCallbacks: PreloadCallbacks
   authInterrupts: boolean
-  StreamingMetadataOutlet: React.ComponentType
+  StreamingMetadataOutlet: React.ComponentType | null
 }): Promise<CacheNodeSeedData> {
   return getTracer().trace(
     NextNodeServerSpan.createComponentTree,
@@ -395,7 +395,9 @@ async function createComponentTreeInternal({
   // Use the same condition to render metadataOutlet as metadata
   const metadataOutlet = StreamingMetadataOutlet ? (
     <StreamingMetadataOutlet />
-  ) : undefined
+  ) : (
+    <MetadataOutlet ready={getMetadataReady} />
+  )
 
   const notFoundElement = NotFound ? (
     <>
@@ -716,9 +718,6 @@ async function createComponentTreeInternal({
         {layerAssets}
         <OutletBoundary>
           <MetadataOutlet ready={getViewportReady} />
-          {/* Blocking metadata outlet */}
-          <MetadataOutlet ready={getMetadataReady} />
-          {/* Streaming metadata outlet */}
           {metadataOutlet}
         </OutletBoundary>
       </React.Fragment>,

filename: packages/next/src/server/app-render/walk-tree-with-flight-router-state.tsx
patch: @@ -56,7 +56,7 @@ export async function walkTreeWithFlightRouterState({
   getViewportReady: () => Promise<void>
   ctx: AppRenderContext
   preloadCallbacks: PreloadCallbacks
-  StreamingMetadataOutlet: React.ComponentType
+  StreamingMetadataOutlet: React.ComponentType | null
 }): Promise<FlightDataPath[]> {
   const {
     renderOpts: { nextFontManifest, experimental },

filename: test/e2e/app-dir/metadata-streaming-parallel-routes/metadata-streaming-parallel-routes.test.ts
patch: @@ -9,13 +9,12 @@ describe('app-dir - metadata-streaming', () => {
   it('should only insert metadata once for parallel routes when slots match', async () => {
     const browser = await next.browser('/parallel-routes')
 
-    expect((await browser.elementsByCss('head title')).length).toBe(1)
-    expect((await browser.elementsByCss('body title')).length).toBe(0)
+    expect((await browser.elementsByCss('title')).length).toBe(1)
     expect(await browser.elementByCss('title').text()).toBe('parallel title')
 
     const $ = await next.render$('/parallel-routes')
     expect($('title').length).toBe(1)
-    // We can't ensure if it's inserted into  head or body since it's a race condition,
+    // We can't ensure if it's inserted into head or body since it's a race condition,
     // where sometimes the metadata can be suspended.
     expect($('title').text()).toBe('parallel title')
 
@@ -51,7 +50,7 @@ describe('app-dir - metadata-streaming', () => {
     const browser = await next.browser('/parallel-routes-default')
 
     expect((await browser.elementsByCss('title')).length).toBe(1)
-    expect(await browser.elementByCss('body title').text()).toBe(
+    expect(await browser.elementByCss('title').text()).toBe(
       'parallel-routes-default layout title'
     )