From b80e0f6ce37127a52ca913191e36a3bf153a580a Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Mon, 12 Aug 2024 23:46:49 +0200 Subject: [PATCH] Always collect static icons for all segments (#68712) ### What Collect static metadata icons from the most leaf node in the component tree, and then add them into resolved metadata icons if there's no icons presented in exported metadata ### Why Previously we collected the icons from the last item from collected metadata items as last segment in the tree. But it doesn't act like that when there's parallel routes, so we collect the last presented static metadata icons from the metadata item list and then merge into the resolved metadata at the end. Fixes #68650 --- .../next/src/lib/metadata/resolve-metadata.ts | 61 ++++++++++++------ .../app/@modal/default.js | 3 + .../app/apple-icon.png | Bin 0 -> 1661 bytes .../app/favicon.ico | Bin 0 -> 15086 bytes .../app/icon.svg | 14 ++++ .../app/layout.js | 10 +++ .../app/nested/icon.png | Bin 0 -> 1661 bytes .../app/nested/page.js | 3 + .../app/page.js | 3 + .../app/test.png | Bin 0 -> 1661 bytes .../metadata-icons-parallel-routes.test.ts | 25 +++++++ 11 files changed, 98 insertions(+), 21 deletions(-) create mode 100644 test/e2e/app-dir/metadata-icons-parallel-routes/app/@modal/default.js create mode 100644 test/e2e/app-dir/metadata-icons-parallel-routes/app/apple-icon.png create mode 100644 test/e2e/app-dir/metadata-icons-parallel-routes/app/favicon.ico create mode 100644 test/e2e/app-dir/metadata-icons-parallel-routes/app/icon.svg create mode 100644 test/e2e/app-dir/metadata-icons-parallel-routes/app/layout.js create mode 100644 test/e2e/app-dir/metadata-icons-parallel-routes/app/nested/icon.png create mode 100644 test/e2e/app-dir/metadata-icons-parallel-routes/app/nested/page.js create mode 100644 test/e2e/app-dir/metadata-icons-parallel-routes/app/page.js create mode 100644 test/e2e/app-dir/metadata-icons-parallel-routes/app/test.png create mode 100644 test/e2e/app-dir/metadata-icons-parallel-routes/metadata-icons-parallel-routes.test.ts diff --git a/packages/next/src/lib/metadata/resolve-metadata.ts b/packages/next/src/lib/metadata/resolve-metadata.ts index aa546db407baa..59cf33be3672d 100644 --- a/packages/next/src/lib/metadata/resolve-metadata.ts +++ b/packages/next/src/lib/metadata/resolve-metadata.ts @@ -16,6 +16,7 @@ import type { LoaderTree } from '../../server/lib/app-dir-module' import type { AbsoluteTemplateString, IconDescriptor, + ResolvedIcons, } from './types/metadata-types' import type { ParsedUrlQuery } from 'querystring' import type { StaticMetadata } from './types/icons' @@ -49,6 +50,8 @@ import { ResolveMetadataSpan } from '../../server/lib/trace/constants' import { PAGE_SEGMENT_KEY } from '../../shared/lib/segment' import * as Log from '../../build/output/log' +type StaticIcons = Pick + type MetadataResolver = ( parent: ResolvingMetadata ) => Metadata | Promise @@ -99,27 +102,18 @@ function mergeStaticMetadata( staticFilesMetadata: StaticMetadata, metadataContext: MetadataContext, titleTemplates: TitleTemplates, - isLastSegment: boolean + leafSegmentStaticIcons: StaticIcons ) { if (!staticFilesMetadata) return const { icon, apple, openGraph, twitter, manifest } = staticFilesMetadata - // Only pick up the static metadata if the current level is the last segment - if (isLastSegment) { - // file based metadata is specified and current level metadata icons is not specified - if (target.icons) { - if (icon) { - target.icons.icon.unshift(...icon) - } - if (apple) { - target.icons.apple.unshift(...apple) - } - } else if (icon || apple) { - target.icons = { - icon: icon || [], - apple: apple || [], - } - } + // Keep updating the static icons in the most leaf node + + if (icon) { + leafSegmentStaticIcons.icon = icon + } + if (apple) { + leafSegmentStaticIcons.apple = apple } // file based metadata is specified and current level metadata twitter.images is not specified @@ -158,7 +152,7 @@ function mergeMetadata({ titleTemplates, metadataContext, buildState, - isLastSegment, + leafSegmentStaticIcons, }: { source: Metadata | null target: ResolvedMetadata @@ -166,7 +160,7 @@ function mergeMetadata({ titleTemplates: TitleTemplates metadataContext: MetadataContext buildState: BuildState - isLastSegment: boolean + leafSegmentStaticIcons: StaticIcons }): void { // If there's override metadata, prefer it otherwise fallback to the default metadata. const metadataBase = @@ -290,7 +284,7 @@ function mergeMetadata({ staticFilesMetadata, metadataContext, titleTemplates, - isLastSegment + leafSegmentStaticIcons ) } @@ -774,6 +768,13 @@ export async function accumulateMetadata( } let favicon + + // Collect the static icons in the most leaf node, + // since we don't collect all the static metadata icons in the parent segments. + const leafSegmentStaticIcons = { + icon: [], + apple: [], + } for (let i = 0; i < metadataItems.length; i++) { const staticFilesMetadata = metadataItems[i][1] @@ -800,7 +801,7 @@ export async function accumulateMetadata( staticFilesMetadata, titleTemplates, buildState, - isLastSegment: i === metadataItems.length - 1, + leafSegmentStaticIcons, }) // If the layout is the same layer with page, skip the leaf layout and leaf page @@ -814,6 +815,24 @@ export async function accumulateMetadata( } } + if ( + leafSegmentStaticIcons.icon.length > 0 || + leafSegmentStaticIcons.apple.length > 0 + ) { + if (!resolvedMetadata.icons) { + resolvedMetadata.icons = { + icon: [], + apple: [], + } + if (leafSegmentStaticIcons.icon.length > 0) { + resolvedMetadata.icons.icon.unshift(...leafSegmentStaticIcons.icon) + } + if (leafSegmentStaticIcons.apple.length > 0) { + resolvedMetadata.icons.apple.unshift(...leafSegmentStaticIcons.apple) + } + } + } + // Only log warnings if there are any, and only once after the metadata resolving process is finished if (buildState.warnings.size > 0) { for (const warning of buildState.warnings) { diff --git a/test/e2e/app-dir/metadata-icons-parallel-routes/app/@modal/default.js b/test/e2e/app-dir/metadata-icons-parallel-routes/app/@modal/default.js new file mode 100644 index 0000000000000..a40457174b47f --- /dev/null +++ b/test/e2e/app-dir/metadata-icons-parallel-routes/app/@modal/default.js @@ -0,0 +1,3 @@ +export default function Modal() { + return

modal

+} diff --git a/test/e2e/app-dir/metadata-icons-parallel-routes/app/apple-icon.png b/test/e2e/app-dir/metadata-icons-parallel-routes/app/apple-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..7cbc1d26733614b83dcc25b9cfcd5e06dffb147c GIT binary patch literal 1661 zcmV-@27>vCP)LUUqAigYBsiM4LpbwXrcM#DLg%sv zqYV7atJ2!&9+4a}Z~>mx*QQG8O{j2%jKM&gp$l9QGmOhXo1ym@G1NiGAj_1lhANDV z1!S4frxq9i3di1^l$}15Gz3>C_2kBUE^I5>7jp>X5wdo&Pd!2a6f5GP)GgXa3QSQ5 zJ0>bk8Vt=;h$f0RQz4ot-b{sPqIfeEqKV?oREQ=HW#$APi4kKst#JY0qCFI7KO!;| z-{Dx5|6)LB*iF*ATe1cl&QpkEr#`hqi_TA*>5v*DyuQX9nwI%PlNUBUM=lW0iy@%% zB^BF?>xE4!QsG#Qk#JB|Zt7m}4cuf9&Sc^>nhincOJ6g{>L6IPUat?vOi^vN=pG%= zK#)fFriX`z1CgewsCHXKz1WKmEejx6qkEB3>Y%mGjoj^a`R9__Z&8YtD2$*QSF6?G zlq}0cwcn!FRXGQOH@cIk`fMagBI@t3C`Ai25VWzK_5A#7HS(IOas~u%Y$wfTvqK&8 z`CQcBV^O1VSn-#r$_`Ey)bSA>M>05=GHK|=D&vNW5yX)MHLMsk@M4|D4HqMbBTuM7 zGiqpSoyQFq!aF`YRFE)gWNTH$4Hv@WNCFEhW{qs6s<<)osDkweD~7ZT#gP4NM1Ob2K1o@yQY z{T_FBclnkpP431DijTPALP{KAd${l!H(UsaBYd9=;5%-(VBYcJ89zeB4Hrz~h-Q0^ ze0=2jeA>lg0fywgunf%JVWH!O3x;tdBO_PL8pWoAQw77TBTG88Zovo+al-|}I6`Y+ z^0|$UQ`~UDEROt4MzXmBi(A}q!6=R-WF(8pS6t(U3np>o%N@)*bYntY-RKH~s(kZ& z*Hy^IaE}`<0>_c%sgbjQ4H479sUmP3xhDh5k)0O=LPFed5jc*#o_|5Z^Sen>+;9;n zj%ZkT@@=HFxZxr|$7g9XGV$36sd2-F=Qxs}0&mwQrN#{x9;;HL1}}$K>2bq_r#P}a zHSYXT*aJ@Mua=>9?N1|Wq^)( z8lD&1iZo_xba(zmTSe0Rk3SIMiq)H{bw6TN@?>lD0hU=ERkP?~Rr2IdR7}mOTZt!D zB`>zHoEuB(=Cx8){)z|}QgkGPtc*2AX*g)I$Qez}x@fuBNN>;~bzAp>Cd&@~hoj*M zA|d$Q=r7lc+AIW0RXIh(CtK8Xa87A+6VERwZWf7nf%f%>rY~-@>N)-3|BZe^#0S3_ z{euDVJ|*M|Rl5{KpJV_IQG>{9LMTUlr0g6i(g^TuK!s?gcoRz$qM71NEK!JNiZ`)D zA(|=P#1e&Qrg#%e6h^R+s0U>ba-BC08dS?$*zXIv&y4Nk(E@0PhHE2pL?NCxNoxpO z1fYr(vUYr*N-;7NuB$pTjQslT4C6MG&V~25B2tXY&|UF2UFGyqg=0@nLzZdLz#3+d zHJFeYJs=ub33U)w;8~?qeH7>uL|Ty`ys7lN*_+p9I%H@Rjysd|J%;PrZ*hEp)N8Tb zLI9GCJcrIr)06Ejj%H5^Da-fl@pVe)w$P_yyD7FJr+j_}po~WmWUki$00000NkvXX Hu0mjfdKc_u literal 0 HcmV?d00001 diff --git a/test/e2e/app-dir/metadata-icons-parallel-routes/app/favicon.ico b/test/e2e/app-dir/metadata-icons-parallel-routes/app/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..4965832f2c9b0605eaa189b7c7fb11124d24e48a GIT binary patch literal 15086 zcmeHOOH5Q(7(R0cc?bh2AT>N@1PWL!LLfZKyG5c!MTHoP7_p!sBz0k$?pjS;^lmgJ zU6^i~bWuZYHL)9$wuvEKm~qo~(5=Lvx5&Hv;?X#m}i|`yaGY4gX+&b>tew;gcnRQA1kp zBbm04SRuuE{Hn+&1wk%&g;?wja_Is#1gKoFlI7f`Gt}X*-nsMO30b_J@)EFNhzd1QM zdH&qFb9PVqQOx@clvc#KAu}^GrN`q5oP(8>m4UOcp`k&xwzkTio*p?kI4BPtIwX%B zJN69cGsm=x90<;Wmh-bs>43F}ro$}Of@8)4KHndLiR$nW?*{Rl72JPUqRr3ta6e#A z%DTEbi9N}+xPtd1juj8;(CJt3r9NOgb>KTuK|z7!JB_KsFW3(pBN4oh&M&}Nb$Ee2 z$-arA6a)CdsPj`M#1DS>fqj#KF%0q?w50GN4YbmMZIoF{e1yTR=4ablqXHBB2!`wM z1M1ke9+<);|AI;f=2^F1;G6Wfpql?1d5D4rMr?#f(=hkoH)U`6Gb)#xDLjoKjp)1;Js@2Iy5yk zMXUqj+gyk1i0yLjWS|3sM2-1ECc;MAz<4t0P53%7se$$+5Ex`L5TQO_MMXXi04UDIU+3*7Ez&X|mj9cFYBXqM{M;mw_ zpw>azP*qjMyNSD4hh)XZt$gqf8f?eRSFX8VQ4Y+H3jAtvyTrXr`qHAD6`m;aYmH2zOhJC~_*AuT} zvUxC38|JYN94i(05R)dVKgUQF$}#cxV7xZ4FULqFCNX*Forhgp*yr6;DsIk=ub0Hv zpk2L{9Q&|uI^b<6@i(Y+iSxeO_n**4nRLc`P!3ld5jL=nZRw6;DEJ*1z6Pvg+eW|$lnnjO zjd|8>6l{i~UxI244CGn2kK@cJ|#ecwgSyt&HKA2)z zrOO{op^o*- + + + + + + + + + + + + + diff --git a/test/e2e/app-dir/metadata-icons-parallel-routes/app/layout.js b/test/e2e/app-dir/metadata-icons-parallel-routes/app/layout.js new file mode 100644 index 0000000000000..d0fba92eee19e --- /dev/null +++ b/test/e2e/app-dir/metadata-icons-parallel-routes/app/layout.js @@ -0,0 +1,10 @@ +export default function Root({ children, modal }) { + return ( + + + {modal} + {children} + + + ) +} diff --git a/test/e2e/app-dir/metadata-icons-parallel-routes/app/nested/icon.png b/test/e2e/app-dir/metadata-icons-parallel-routes/app/nested/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..7cbc1d26733614b83dcc25b9cfcd5e06dffb147c GIT binary patch literal 1661 zcmV-@27>vCP)LUUqAigYBsiM4LpbwXrcM#DLg%sv zqYV7atJ2!&9+4a}Z~>mx*QQG8O{j2%jKM&gp$l9QGmOhXo1ym@G1NiGAj_1lhANDV z1!S4frxq9i3di1^l$}15Gz3>C_2kBUE^I5>7jp>X5wdo&Pd!2a6f5GP)GgXa3QSQ5 zJ0>bk8Vt=;h$f0RQz4ot-b{sPqIfeEqKV?oREQ=HW#$APi4kKst#JY0qCFI7KO!;| z-{Dx5|6)LB*iF*ATe1cl&QpkEr#`hqi_TA*>5v*DyuQX9nwI%PlNUBUM=lW0iy@%% zB^BF?>xE4!QsG#Qk#JB|Zt7m}4cuf9&Sc^>nhincOJ6g{>L6IPUat?vOi^vN=pG%= zK#)fFriX`z1CgewsCHXKz1WKmEejx6qkEB3>Y%mGjoj^a`R9__Z&8YtD2$*QSF6?G zlq}0cwcn!FRXGQOH@cIk`fMagBI@t3C`Ai25VWzK_5A#7HS(IOas~u%Y$wfTvqK&8 z`CQcBV^O1VSn-#r$_`Ey)bSA>M>05=GHK|=D&vNW5yX)MHLMsk@M4|D4HqMbBTuM7 zGiqpSoyQFq!aF`YRFE)gWNTH$4Hv@WNCFEhW{qs6s<<)osDkweD~7ZT#gP4NM1Ob2K1o@yQY z{T_FBclnkpP431DijTPALP{KAd${l!H(UsaBYd9=;5%-(VBYcJ89zeB4Hrz~h-Q0^ ze0=2jeA>lg0fywgunf%JVWH!O3x;tdBO_PL8pWoAQw77TBTG88Zovo+al-|}I6`Y+ z^0|$UQ`~UDEROt4MzXmBi(A}q!6=R-WF(8pS6t(U3np>o%N@)*bYntY-RKH~s(kZ& z*Hy^IaE}`<0>_c%sgbjQ4H479sUmP3xhDh5k)0O=LPFed5jc*#o_|5Z^Sen>+;9;n zj%ZkT@@=HFxZxr|$7g9XGV$36sd2-F=Qxs}0&mwQrN#{x9;;HL1}}$K>2bq_r#P}a zHSYXT*aJ@Mua=>9?N1|Wq^)( z8lD&1iZo_xba(zmTSe0Rk3SIMiq)H{bw6TN@?>lD0hU=ERkP?~Rr2IdR7}mOTZt!D zB`>zHoEuB(=Cx8){)z|}QgkGPtc*2AX*g)I$Qez}x@fuBNN>;~bzAp>Cd&@~hoj*M zA|d$Q=r7lc+AIW0RXIh(CtK8Xa87A+6VERwZWf7nf%f%>rY~-@>N)-3|BZe^#0S3_ z{euDVJ|*M|Rl5{KpJV_IQG>{9LMTUlr0g6i(g^TuK!s?gcoRz$qM71NEK!JNiZ`)D zA(|=P#1e&Qrg#%e6h^R+s0U>ba-BC08dS?$*zXIv&y4Nk(E@0PhHE2pL?NCxNoxpO z1fYr(vUYr*N-;7NuB$pTjQslT4C6MG&V~25B2tXY&|UF2UFGyqg=0@nLzZdLz#3+d zHJFeYJs=ub33U)w;8~?qeH7>uL|Ty`ys7lN*_+p9I%H@Rjysd|J%;PrZ*hEp)N8Tb zLI9GCJcrIr)06Ejj%H5^Da-fl@pVe)w$P_yyD7FJr+j_}po~WmWUki$00000NkvXX Hu0mjfdKc_u literal 0 HcmV?d00001 diff --git a/test/e2e/app-dir/metadata-icons-parallel-routes/app/nested/page.js b/test/e2e/app-dir/metadata-icons-parallel-routes/app/nested/page.js new file mode 100644 index 0000000000000..f9cb981736359 --- /dev/null +++ b/test/e2e/app-dir/metadata-icons-parallel-routes/app/nested/page.js @@ -0,0 +1,3 @@ +export default function Page() { + return

page

+} diff --git a/test/e2e/app-dir/metadata-icons-parallel-routes/app/page.js b/test/e2e/app-dir/metadata-icons-parallel-routes/app/page.js new file mode 100644 index 0000000000000..f9cb981736359 --- /dev/null +++ b/test/e2e/app-dir/metadata-icons-parallel-routes/app/page.js @@ -0,0 +1,3 @@ +export default function Page() { + return

page

+} diff --git a/test/e2e/app-dir/metadata-icons-parallel-routes/app/test.png b/test/e2e/app-dir/metadata-icons-parallel-routes/app/test.png new file mode 100644 index 0000000000000000000000000000000000000000..7cbc1d26733614b83dcc25b9cfcd5e06dffb147c GIT binary patch literal 1661 zcmV-@27>vCP)LUUqAigYBsiM4LpbwXrcM#DLg%sv zqYV7atJ2!&9+4a}Z~>mx*QQG8O{j2%jKM&gp$l9QGmOhXo1ym@G1NiGAj_1lhANDV z1!S4frxq9i3di1^l$}15Gz3>C_2kBUE^I5>7jp>X5wdo&Pd!2a6f5GP)GgXa3QSQ5 zJ0>bk8Vt=;h$f0RQz4ot-b{sPqIfeEqKV?oREQ=HW#$APi4kKst#JY0qCFI7KO!;| z-{Dx5|6)LB*iF*ATe1cl&QpkEr#`hqi_TA*>5v*DyuQX9nwI%PlNUBUM=lW0iy@%% zB^BF?>xE4!QsG#Qk#JB|Zt7m}4cuf9&Sc^>nhincOJ6g{>L6IPUat?vOi^vN=pG%= zK#)fFriX`z1CgewsCHXKz1WKmEejx6qkEB3>Y%mGjoj^a`R9__Z&8YtD2$*QSF6?G zlq}0cwcn!FRXGQOH@cIk`fMagBI@t3C`Ai25VWzK_5A#7HS(IOas~u%Y$wfTvqK&8 z`CQcBV^O1VSn-#r$_`Ey)bSA>M>05=GHK|=D&vNW5yX)MHLMsk@M4|D4HqMbBTuM7 zGiqpSoyQFq!aF`YRFE)gWNTH$4Hv@WNCFEhW{qs6s<<)osDkweD~7ZT#gP4NM1Ob2K1o@yQY z{T_FBclnkpP431DijTPALP{KAd${l!H(UsaBYd9=;5%-(VBYcJ89zeB4Hrz~h-Q0^ ze0=2jeA>lg0fywgunf%JVWH!O3x;tdBO_PL8pWoAQw77TBTG88Zovo+al-|}I6`Y+ z^0|$UQ`~UDEROt4MzXmBi(A}q!6=R-WF(8pS6t(U3np>o%N@)*bYntY-RKH~s(kZ& z*Hy^IaE}`<0>_c%sgbjQ4H479sUmP3xhDh5k)0O=LPFed5jc*#o_|5Z^Sen>+;9;n zj%ZkT@@=HFxZxr|$7g9XGV$36sd2-F=Qxs}0&mwQrN#{x9;;HL1}}$K>2bq_r#P}a zHSYXT*aJ@Mua=>9?N1|Wq^)( z8lD&1iZo_xba(zmTSe0Rk3SIMiq)H{bw6TN@?>lD0hU=ERkP?~Rr2IdR7}mOTZt!D zB`>zHoEuB(=Cx8){)z|}QgkGPtc*2AX*g)I$Qez}x@fuBNN>;~bzAp>Cd&@~hoj*M zA|d$Q=r7lc+AIW0RXIh(CtK8Xa87A+6VERwZWf7nf%f%>rY~-@>N)-3|BZe^#0S3_ z{euDVJ|*M|Rl5{KpJV_IQG>{9LMTUlr0g6i(g^TuK!s?gcoRz$qM71NEK!JNiZ`)D zA(|=P#1e&Qrg#%e6h^R+s0U>ba-BC08dS?$*zXIv&y4Nk(E@0PhHE2pL?NCxNoxpO z1fYr(vUYr*N-;7NuB$pTjQslT4C6MG&V~25B2tXY&|UF2UFGyqg=0@nLzZdLz#3+d zHJFeYJs=ub33U)w;8~?qeH7>uL|Ty`ys7lN*_+p9I%H@Rjysd|J%;PrZ*hEp)N8Tb zLI9GCJcrIr)06Ejj%H5^Da-fl@pVe)w$P_yyD7FJr+j_}po~WmWUki$00000NkvXX Hu0mjfdKc_u literal 0 HcmV?d00001 diff --git a/test/e2e/app-dir/metadata-icons-parallel-routes/metadata-icons-parallel-routes.test.ts b/test/e2e/app-dir/metadata-icons-parallel-routes/metadata-icons-parallel-routes.test.ts new file mode 100644 index 0000000000000..1ebd0b77b5885 --- /dev/null +++ b/test/e2e/app-dir/metadata-icons-parallel-routes/metadata-icons-parallel-routes.test.ts @@ -0,0 +1,25 @@ +import { nextTestSetup } from 'e2e-utils' + +describe('app-dir - metadata-icons-parallel-routes', () => { + const { next } = nextTestSetup({ + files: __dirname, + }) + + it('should present favicon with other icons when parallel routes are presented', async () => { + const $ = await next.render$('/') + expect($('link[type="image/x-icon"]').length).toBe(1) + expect($('link[type="image/svg+xml"]').length).toBe(1) + expect($('link[rel="apple-touch-icon"]').length).toBe(1) + }) + + it('should override parent icon when both static icon presented', async () => { + const $ = await next.render$('/nested') + expect($('link[type="image/x-icon"]').length).toBe(1) + expect($('link[rel="icon"][type="image/png"]').length).toBe(1) + }) + + it('should inherit parent apple icon when child does not present but parent contain static apple icon', async () => { + const $ = await next.render$('/nested') + expect($('link[rel="apple-touch-icon"][type="image/png"]').length).toBe(1) + }) +})