Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 11cfdea

Browse files
committed
fix icon display in slow streamed metadata
1 parent 2368150 commit 11cfdea

File tree

10 files changed

+201
-15
lines changed

10 files changed

+201
-15
lines changed
File renamed without changes.

packages/next/src/lib/metadata/generate/icons.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import type { ResolvedMetadata } from '../types/metadata-interface'
22
import type { Icon, IconDescriptor } from '../types/metadata-types'
33

4-
import React from 'react'
54
import { MetaFilter } from './meta'
65

76
function IconDescriptorLink({ icon }: { icon: IconDescriptor }) {

packages/next/src/server/app-render/app-render.tsx

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -317,7 +317,7 @@ function createNotFoundLoaderTree(loaderTree: LoaderTree): LoaderTree {
317317
}
318318

319319
function createDivergedMetadataComponents(
320-
Metadata: React.ComponentType<{}>,
320+
Metadata: React.ComponentType,
321321
serveStreamingMetadata: boolean
322322
): {
323323
StaticMetadata: React.ComponentType<{}>
@@ -326,8 +326,9 @@ function createDivergedMetadataComponents(
326326
function EmptyMetadata() {
327327
return null
328328
}
329-
const StreamingMetadata: React.ComponentType<{}> | null =
330-
serveStreamingMetadata ? Metadata : null
329+
const StreamingMetadata: React.ComponentType | null = serveStreamingMetadata
330+
? Metadata
331+
: null
331332

332333
const StaticMetadata: React.ComponentType<{}> = serveStreamingMetadata
333334
? EmptyMetadata
@@ -1934,6 +1935,7 @@ async function renderToStream(
19341935
),
19351936
getServerInsertedHTML,
19361937
getServerInsertedMetadata,
1938+
nonce: ctx.nonce,
19371939
})
19381940
}
19391941
}
@@ -2006,6 +2008,7 @@ async function renderToStream(
20062008
getServerInsertedHTML,
20072009
getServerInsertedMetadata,
20082010
validateRootLayout,
2011+
nonce: ctx.nonce,
20092012
})
20102013
} catch (err) {
20112014
if (
@@ -2157,6 +2160,7 @@ async function renderToStream(
21572160
}),
21582161
getServerInsertedMetadata,
21592162
validateRootLayout,
2163+
nonce: ctx.nonce,
21602164
})
21612165
} catch (finalErr: any) {
21622166
if (
@@ -3088,6 +3092,7 @@ async function prerenderToStream(
30883092
stream: await continueDynamicPrerender(prelude, {
30893093
getServerInsertedHTML,
30903094
getServerInsertedMetadata,
3095+
nonce: ctx.nonce,
30913096
}),
30923097
dynamicAccess: consumeDynamicAccess(
30933098
serverDynamicTracking,
@@ -3150,6 +3155,7 @@ async function prerenderToStream(
31503155
),
31513156
getServerInsertedHTML,
31523157
getServerInsertedMetadata,
3158+
nonce: ctx.nonce,
31533159
}),
31543160
dynamicAccess: consumeDynamicAccess(
31553161
serverDynamicTracking,
@@ -3571,6 +3577,7 @@ async function prerenderToStream(
35713577
getServerInsertedHTML,
35723578
getServerInsertedMetadata,
35733579
validateRootLayout,
3580+
nonce: ctx.nonce,
35743581
}),
35753582
dynamicAccess: consumeDynamicAccess(
35763583
serverDynamicTracking,
@@ -3723,6 +3730,7 @@ async function prerenderToStream(
37233730
stream: await continueDynamicPrerender(prelude, {
37243731
getServerInsertedHTML,
37253732
getServerInsertedMetadata,
3733+
nonce: ctx.nonce,
37263734
}),
37273735
dynamicAccess: dynamicTracking.dynamicAccesses,
37283736
// TODO: Should this include the SSR pass?
@@ -3743,6 +3751,7 @@ async function prerenderToStream(
37433751
stream: await continueDynamicPrerender(prelude, {
37443752
getServerInsertedHTML,
37453753
getServerInsertedMetadata,
3754+
nonce: ctx.nonce,
37463755
}),
37473756
dynamicAccess: dynamicTracking.dynamicAccesses,
37483757
// TODO: Should this include the SSR pass?
@@ -3803,6 +3812,7 @@ async function prerenderToStream(
38033812
),
38043813
getServerInsertedHTML,
38053814
getServerInsertedMetadata,
3815+
nonce: ctx.nonce,
38063816
}),
38073817
dynamicAccess: dynamicTracking.dynamicAccesses,
38083818
// TODO: Should this include the SSR pass?
@@ -3897,6 +3907,7 @@ async function prerenderToStream(
38973907
isStaticGeneration: true,
38983908
getServerInsertedHTML,
38993909
getServerInsertedMetadata,
3910+
nonce: ctx.nonce,
39003911
}),
39013912
// TODO: Should this include the SSR pass?
39023913
collectedRevalidate: prerenderLegacyStore.revalidate,
@@ -4074,6 +4085,7 @@ async function prerenderToStream(
40744085
}),
40754086
getServerInsertedMetadata,
40764087
validateRootLayout,
4088+
nonce: ctx.nonce,
40774089
}),
40784090
dynamicAccess: null,
40794091
collectedRevalidate:

packages/next/src/server/app-render/create-component-tree.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export function createComponentTree(props: {
4040
missingSlots?: Set<string>
4141
preloadCallbacks: PreloadCallbacks
4242
authInterrupts: boolean
43-
StreamingMetadata: React.ComponentType<{}> | null
43+
StreamingMetadata: React.ComponentType | null
4444
StreamingMetadataOutlet: React.ComponentType
4545
}): Promise<CacheNodeSeedData> {
4646
return getTracer().trace(
@@ -92,7 +92,7 @@ async function createComponentTreeInternal({
9292
missingSlots?: Set<string>
9393
preloadCallbacks: PreloadCallbacks
9494
authInterrupts: boolean
95-
StreamingMetadata: React.ComponentType<{}> | null
95+
StreamingMetadata: React.ComponentType | null
9696
StreamingMetadataOutlet: React.ComponentType | null
9797
}): Promise<CacheNodeSeedData> {
9898
const {

packages/next/src/server/stream-utils/node-web-streams-helper.ts

Lines changed: 74 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,14 @@ export function renderToInitialFizzStream({
193193
}
194194

195195
function createHeadInsertionTransformStream(
196-
insert: () => Promise<string>
196+
insert: () => Promise<string>,
197+
{
198+
nonce,
199+
reinsertIcons,
200+
}: {
201+
nonce: string | undefined
202+
reinsertIcons: boolean
203+
}
197204
): TransformStream<Uint8Array, Uint8Array> {
198205
let inserted = false
199206

@@ -264,6 +271,10 @@ function createHeadInsertionTransformStream(
264271
if (insertion) {
265272
controller.enqueue(encoder.encode(insertion))
266273
}
274+
// Insert the icon script at the end of the document.
275+
if (reinsertIcons) {
276+
controller.enqueue(encoder.encode(createReinsertIconScript(nonce)))
277+
}
267278
}
268279
},
269280
})
@@ -378,6 +389,22 @@ function createMergedTransformStream(
378389

379390
const CLOSE_TAG = '</body></html>'
380391

392+
/**
393+
* For chromium based browsers (Chrome, Edge, etc.) and Safari, icons need to stay under <head>
394+
* to be picked up by the browser. Firefox doesn't have this requirement.
395+
*
396+
* Firefox won't work if we insert those icons into head, it will still pick up default favicon.ico.
397+
* Because of this limitation, we just don't insert for firefox and leave the default behavior for it.
398+
*
399+
*/
400+
const createReinsertIconScript = (nonce: string | undefined) => {
401+
return `\
402+
<script ${typeof nonce === 'string' ? `nonce="${nonce}"` : ''}>\
403+
!/firefox/i.test(navigator.userAgent) && \
404+
document.querySelectorAll('body link[rel="icon"], body link[rel="apple-touch-icon"]').forEach(el => document.head.appendChild(el.cloneNode()))\
405+
</script>`
406+
}
407+
381408
/**
382409
* This transform stream moves the suffix to the end of the stream, so results
383410
* like `</body></html><script>...</script>` will be transformed to
@@ -529,6 +556,7 @@ export type ContinueStreamOptions = {
529556
* Suffix to inject after the buffered data, but before the close tags.
530557
*/
531558
suffix?: string | undefined
559+
nonce: string | undefined
532560
}
533561

534562
export async function continueFizzStream(
@@ -540,6 +568,7 @@ export async function continueFizzStream(
540568
getServerInsertedHTML,
541569
getServerInsertedMetadata,
542570
validateRootLayout,
571+
nonce,
543572
}: ContinueStreamOptions
544573
): Promise<ReadableStream<Uint8Array>> {
545574
// Suffix itself might contain close tags at the end, so we need to split it.
@@ -556,7 +585,10 @@ export async function continueFizzStream(
556585
createBufferedTransformStream(),
557586

558587
// Insert generated metadata
559-
createHeadInsertionTransformStream(getServerInsertedMetadata),
588+
createHeadInsertionTransformStream(getServerInsertedMetadata, {
589+
nonce,
590+
reinsertIcons: true,
591+
}),
560592

561593
// Insert suffix content
562594
suffixUnclosed != null && suffixUnclosed.length > 0
@@ -575,20 +607,25 @@ export async function continueFizzStream(
575607
// Special head insertions
576608
// TODO-APP: Insert server side html to end of head in app layout rendering, to avoid
577609
// hydration errors. Remove this once it's ready to be handled by react itself.
578-
createHeadInsertionTransformStream(getServerInsertedHTML),
610+
createHeadInsertionTransformStream(getServerInsertedHTML, {
611+
nonce,
612+
reinsertIcons: false,
613+
}),
579614
])
580615
}
581616

582617
type ContinueDynamicPrerenderOptions = {
583618
getServerInsertedHTML: () => Promise<string>
584619
getServerInsertedMetadata: () => Promise<string>
620+
nonce: string | undefined
585621
}
586622

587623
export async function continueDynamicPrerender(
588624
prerenderStream: ReadableStream<Uint8Array>,
589625
{
590626
getServerInsertedHTML,
591627
getServerInsertedMetadata,
628+
nonce,
592629
}: ContinueDynamicPrerenderOptions
593630
) {
594631
return (
@@ -597,10 +634,18 @@ export async function continueDynamicPrerender(
597634
.pipeThrough(createBufferedTransformStream())
598635
.pipeThrough(createStripDocumentClosingTagsTransform())
599636
// Insert generated tags to head
600-
.pipeThrough(createHeadInsertionTransformStream(getServerInsertedHTML))
637+
.pipeThrough(
638+
createHeadInsertionTransformStream(getServerInsertedHTML, {
639+
nonce,
640+
reinsertIcons: false,
641+
})
642+
)
601643
// Insert generated metadata
602644
.pipeThrough(
603-
createHeadInsertionTransformStream(getServerInsertedMetadata)
645+
createHeadInsertionTransformStream(getServerInsertedMetadata, {
646+
nonce,
647+
reinsertIcons: true,
648+
})
604649
)
605650
)
606651
}
@@ -609,6 +654,7 @@ type ContinueStaticPrerenderOptions = {
609654
inlinedDataStream: ReadableStream<Uint8Array>
610655
getServerInsertedHTML: () => Promise<string>
611656
getServerInsertedMetadata: () => Promise<string>
657+
nonce: string | undefined
612658
}
613659

614660
export async function continueStaticPrerender(
@@ -617,17 +663,26 @@ export async function continueStaticPrerender(
617663
inlinedDataStream,
618664
getServerInsertedHTML,
619665
getServerInsertedMetadata,
666+
nonce,
620667
}: ContinueStaticPrerenderOptions
621668
) {
622669
return (
623670
prerenderStream
624671
// Buffer everything to avoid flushing too frequently
625672
.pipeThrough(createBufferedTransformStream())
626673
// Insert generated tags to head
627-
.pipeThrough(createHeadInsertionTransformStream(getServerInsertedHTML))
674+
.pipeThrough(
675+
createHeadInsertionTransformStream(getServerInsertedHTML, {
676+
nonce,
677+
reinsertIcons: false,
678+
})
679+
)
628680
// Insert generated metadata to head
629681
.pipeThrough(
630-
createHeadInsertionTransformStream(getServerInsertedMetadata)
682+
createHeadInsertionTransformStream(getServerInsertedMetadata, {
683+
nonce,
684+
reinsertIcons: true,
685+
})
631686
)
632687
// Insert the inlined data (Flight data, form state, etc.) stream into the HTML
633688
.pipeThrough(createMergedTransformStream(inlinedDataStream))
@@ -640,6 +695,7 @@ type ContinueResumeOptions = {
640695
inlinedDataStream: ReadableStream<Uint8Array>
641696
getServerInsertedHTML: () => Promise<string>
642697
getServerInsertedMetadata: () => Promise<string>
698+
nonce: string | undefined
643699
}
644700

645701
export async function continueDynamicHTMLResume(
@@ -648,17 +704,26 @@ export async function continueDynamicHTMLResume(
648704
inlinedDataStream,
649705
getServerInsertedHTML,
650706
getServerInsertedMetadata,
707+
nonce,
651708
}: ContinueResumeOptions
652709
) {
653710
return (
654711
renderStream
655712
// Buffer everything to avoid flushing too frequently
656713
.pipeThrough(createBufferedTransformStream())
657714
// Insert generated tags to head
658-
.pipeThrough(createHeadInsertionTransformStream(getServerInsertedHTML))
715+
.pipeThrough(
716+
createHeadInsertionTransformStream(getServerInsertedHTML, {
717+
nonce,
718+
reinsertIcons: false,
719+
})
720+
)
659721
// Insert generated metadata to body
660722
.pipeThrough(
661-
createHeadInsertionTransformStream(getServerInsertedMetadata)
723+
createHeadInsertionTransformStream(getServerInsertedMetadata, {
724+
nonce,
725+
reinsertIcons: true,
726+
})
662727
)
663728
// Insert the inlined data (Flight data, form state, etc.) stream into the HTML
664729
.pipeThrough(createMergedTransformStream(inlinedDataStream))
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { Metadata } from 'next'
2+
import Link from 'next/link'
3+
import { connection } from 'next/server'
4+
5+
export default function Page() {
6+
return (
7+
<>
8+
<Link id="custom-icon-sub-link" href="/custom-icon/sub">
9+
Go to another page with custom icon
10+
</Link>
11+
</>
12+
)
13+
}
14+
15+
export async function generateMetadata(): Promise<Metadata> {
16+
await connection()
17+
await new Promise((resolve) => setTimeout(resolve, 300))
18+
19+
return {
20+
icons: {
21+
// add version query to avoid caching on client side with multiple navs
22+
icon: `/heart.png?v=${Math.round(Math.random() * 1000)}`,
23+
},
24+
}
25+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { type Metadata } from 'next'
2+
import Link from 'next/link'
3+
import { connection } from 'next/server'
4+
5+
export async function generateMetadata(): Promise<Metadata> {
6+
await connection()
7+
await new Promise((resolve) => setTimeout(resolve, 300))
8+
9+
return {
10+
icons: {
11+
// add version query to avoid caching on client side with multiple navs
12+
icon: `/star.png?v=${Math.round(Math.random() * 1000)}`,
13+
},
14+
}
15+
}
16+
17+
export default function Page() {
18+
return (
19+
<>
20+
<Link id="custom-icon-link" href="/custom-icon">
21+
Back to previous page with custom icon
22+
</Link>
23+
</>
24+
)
25+
}

0 commit comments

Comments
 (0)