@@ -193,7 +193,14 @@ export function renderToInitialFizzStream({
193
193
}
194
194
195
195
function createHeadInsertionTransformStream (
196
- insert : ( ) => Promise < string >
196
+ insert : ( ) => Promise < string > ,
197
+ {
198
+ nonce,
199
+ reinsertIcons,
200
+ } : {
201
+ nonce : string | undefined
202
+ reinsertIcons : boolean
203
+ }
197
204
) : TransformStream < Uint8Array , Uint8Array > {
198
205
let inserted = false
199
206
@@ -264,6 +271,10 @@ function createHeadInsertionTransformStream(
264
271
if ( insertion ) {
265
272
controller . enqueue ( encoder . encode ( insertion ) )
266
273
}
274
+ // Insert the icon script at the end of the document.
275
+ if ( reinsertIcons ) {
276
+ controller . enqueue ( encoder . encode ( createReinsertIconScript ( nonce ) ) )
277
+ }
267
278
}
268
279
} ,
269
280
} )
@@ -378,6 +389,22 @@ function createMergedTransformStream(
378
389
379
390
const CLOSE_TAG = '</body></html>'
380
391
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
+
381
408
/**
382
409
* This transform stream moves the suffix to the end of the stream, so results
383
410
* like `</body></html><script>...</script>` will be transformed to
@@ -529,6 +556,7 @@ export type ContinueStreamOptions = {
529
556
* Suffix to inject after the buffered data, but before the close tags.
530
557
*/
531
558
suffix ?: string | undefined
559
+ nonce : string | undefined
532
560
}
533
561
534
562
export async function continueFizzStream (
@@ -540,6 +568,7 @@ export async function continueFizzStream(
540
568
getServerInsertedHTML,
541
569
getServerInsertedMetadata,
542
570
validateRootLayout,
571
+ nonce,
543
572
} : ContinueStreamOptions
544
573
) : Promise < ReadableStream < Uint8Array > > {
545
574
// Suffix itself might contain close tags at the end, so we need to split it.
@@ -556,7 +585,10 @@ export async function continueFizzStream(
556
585
createBufferedTransformStream ( ) ,
557
586
558
587
// Insert generated metadata
559
- createHeadInsertionTransformStream ( getServerInsertedMetadata ) ,
588
+ createHeadInsertionTransformStream ( getServerInsertedMetadata , {
589
+ nonce,
590
+ reinsertIcons : true ,
591
+ } ) ,
560
592
561
593
// Insert suffix content
562
594
suffixUnclosed != null && suffixUnclosed . length > 0
@@ -575,20 +607,25 @@ export async function continueFizzStream(
575
607
// Special head insertions
576
608
// TODO-APP: Insert server side html to end of head in app layout rendering, to avoid
577
609
// 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
+ } ) ,
579
614
] )
580
615
}
581
616
582
617
type ContinueDynamicPrerenderOptions = {
583
618
getServerInsertedHTML : ( ) => Promise < string >
584
619
getServerInsertedMetadata : ( ) => Promise < string >
620
+ nonce : string | undefined
585
621
}
586
622
587
623
export async function continueDynamicPrerender (
588
624
prerenderStream : ReadableStream < Uint8Array > ,
589
625
{
590
626
getServerInsertedHTML,
591
627
getServerInsertedMetadata,
628
+ nonce,
592
629
} : ContinueDynamicPrerenderOptions
593
630
) {
594
631
return (
@@ -597,10 +634,18 @@ export async function continueDynamicPrerender(
597
634
. pipeThrough ( createBufferedTransformStream ( ) )
598
635
. pipeThrough ( createStripDocumentClosingTagsTransform ( ) )
599
636
// Insert generated tags to head
600
- . pipeThrough ( createHeadInsertionTransformStream ( getServerInsertedHTML ) )
637
+ . pipeThrough (
638
+ createHeadInsertionTransformStream ( getServerInsertedHTML , {
639
+ nonce,
640
+ reinsertIcons : false ,
641
+ } )
642
+ )
601
643
// Insert generated metadata
602
644
. pipeThrough (
603
- createHeadInsertionTransformStream ( getServerInsertedMetadata )
645
+ createHeadInsertionTransformStream ( getServerInsertedMetadata , {
646
+ nonce,
647
+ reinsertIcons : true ,
648
+ } )
604
649
)
605
650
)
606
651
}
@@ -609,6 +654,7 @@ type ContinueStaticPrerenderOptions = {
609
654
inlinedDataStream : ReadableStream < Uint8Array >
610
655
getServerInsertedHTML : ( ) => Promise < string >
611
656
getServerInsertedMetadata : ( ) => Promise < string >
657
+ nonce : string | undefined
612
658
}
613
659
614
660
export async function continueStaticPrerender (
@@ -617,17 +663,26 @@ export async function continueStaticPrerender(
617
663
inlinedDataStream,
618
664
getServerInsertedHTML,
619
665
getServerInsertedMetadata,
666
+ nonce,
620
667
} : ContinueStaticPrerenderOptions
621
668
) {
622
669
return (
623
670
prerenderStream
624
671
// Buffer everything to avoid flushing too frequently
625
672
. pipeThrough ( createBufferedTransformStream ( ) )
626
673
// Insert generated tags to head
627
- . pipeThrough ( createHeadInsertionTransformStream ( getServerInsertedHTML ) )
674
+ . pipeThrough (
675
+ createHeadInsertionTransformStream ( getServerInsertedHTML , {
676
+ nonce,
677
+ reinsertIcons : false ,
678
+ } )
679
+ )
628
680
// Insert generated metadata to head
629
681
. pipeThrough (
630
- createHeadInsertionTransformStream ( getServerInsertedMetadata )
682
+ createHeadInsertionTransformStream ( getServerInsertedMetadata , {
683
+ nonce,
684
+ reinsertIcons : true ,
685
+ } )
631
686
)
632
687
// Insert the inlined data (Flight data, form state, etc.) stream into the HTML
633
688
. pipeThrough ( createMergedTransformStream ( inlinedDataStream ) )
@@ -640,6 +695,7 @@ type ContinueResumeOptions = {
640
695
inlinedDataStream : ReadableStream < Uint8Array >
641
696
getServerInsertedHTML : ( ) => Promise < string >
642
697
getServerInsertedMetadata : ( ) => Promise < string >
698
+ nonce : string | undefined
643
699
}
644
700
645
701
export async function continueDynamicHTMLResume (
@@ -648,17 +704,26 @@ export async function continueDynamicHTMLResume(
648
704
inlinedDataStream,
649
705
getServerInsertedHTML,
650
706
getServerInsertedMetadata,
707
+ nonce,
651
708
} : ContinueResumeOptions
652
709
) {
653
710
return (
654
711
renderStream
655
712
// Buffer everything to avoid flushing too frequently
656
713
. pipeThrough ( createBufferedTransformStream ( ) )
657
714
// Insert generated tags to head
658
- . pipeThrough ( createHeadInsertionTransformStream ( getServerInsertedHTML ) )
715
+ . pipeThrough (
716
+ createHeadInsertionTransformStream ( getServerInsertedHTML , {
717
+ nonce,
718
+ reinsertIcons : false ,
719
+ } )
720
+ )
659
721
// Insert generated metadata to body
660
722
. pipeThrough (
661
- createHeadInsertionTransformStream ( getServerInsertedMetadata )
723
+ createHeadInsertionTransformStream ( getServerInsertedMetadata , {
724
+ nonce,
725
+ reinsertIcons : true ,
726
+ } )
662
727
)
663
728
// Insert the inlined data (Flight data, form state, etc.) stream into the HTML
664
729
. pipeThrough ( createMergedTransformStream ( inlinedDataStream ) )
0 commit comments