Skip to content

Commit 529f14c

Browse files
huozhiijjk
authored andcommitted
[metadata] replace for initial body icon case (#81688)
1 parent 6de4682 commit 529f14c

File tree

3 files changed

+90
-16
lines changed

3 files changed

+90
-16
lines changed

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

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -263,21 +263,39 @@ function createMetadataTransformStream(
263263
// Check if icon mark is inside <head> tag in the first chunk.
264264
if (chunkIndex === 0) {
265265
closedHeadIndex = indexOfUint8Array(chunk, ENCODED_TAGS.CLOSED.HEAD)
266-
// The mark icon is located in the 1st chunk before the head tag.
267-
// We do not need to insert the script tag in this case because it's in the head.
268-
// Just remove the icon mark from the chunk.
269-
if (iconMarkIndex < closedHeadIndex && iconMarkIndex !== -1) {
270-
const replaced = new Uint8Array(chunk.length - iconMarkLength)
271-
272-
// Remove the icon mark from the chunk.
273-
replaced.set(chunk.subarray(0, iconMarkIndex))
274-
replaced.set(
275-
chunk.subarray(iconMarkIndex + iconMarkLength),
276-
iconMarkIndex
277-
)
278-
chunk = replaced
266+
if (iconMarkIndex !== -1) {
267+
// The mark icon is located in the 1st chunk before the head tag.
268+
// We do not need to insert the script tag in this case because it's in the head.
269+
// Just remove the icon mark from the chunk.
270+
if (iconMarkIndex < closedHeadIndex) {
271+
const replaced = new Uint8Array(chunk.length - iconMarkLength)
272+
273+
// Remove the icon mark from the chunk.
274+
replaced.set(chunk.subarray(0, iconMarkIndex))
275+
replaced.set(
276+
chunk.subarray(iconMarkIndex + iconMarkLength),
277+
iconMarkIndex
278+
)
279+
chunk = replaced
280+
} else {
281+
// The icon mark is after the head tag, replace and insert the script tag at that position.
282+
const insertion = await insert()
283+
const encodedInsertion = encoder.encode(insertion)
284+
const insertionLength = encodedInsertion.length
285+
const replaced = new Uint8Array(
286+
chunk.length - iconMarkLength + insertionLength
287+
)
288+
replaced.set(chunk.subarray(0, iconMarkIndex))
289+
replaced.set(encodedInsertion, iconMarkIndex)
290+
replaced.set(
291+
chunk.subarray(iconMarkIndex + iconMarkLength),
292+
iconMarkIndex + insertionLength
293+
)
294+
chunk = replaced
295+
}
279296
isMarkRemoved = true
280297
}
298+
// If there's no icon mark located, it will be handled later when if present in the following chunks.
281299
} else {
282300
// When it's appeared in the following chunks, we'll need to
283301
// remove the mark and then insert the script tag at that position.
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import Link from 'next/link'
2+
import { connection } from 'next/server'
3+
4+
export default async function Page() {
5+
return (
6+
<div>
7+
<h1>Delay Icons</h1>
8+
<Link id="custom-icon-sub-link" href="/custom-icon/sub">
9+
Go to another page with custom icon
10+
</Link>
11+
<br />
12+
<Link id="custom-icon-sub-link" href="/custom-icon">
13+
Go to root page with custom icon
14+
</Link>
15+
</div>
16+
)
17+
}
18+
19+
export async function generateMetadata() {
20+
await connection()
21+
return {
22+
// This long text description will lead to the metadata being inserted after the head tag.
23+
description: 'long text description'.repeat(1000),
24+
icons: {
25+
icon: `/heart.png`,
26+
},
27+
}
28+
}

test/e2e/app-dir/metadata-icons/metadata-icons.test.ts

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { nextTestSetup } from 'e2e-utils'
22
import { retry } from 'next-test-utils'
33

4+
const iconInsertionScript = `document.querySelectorAll('body link[rel="icon"], body link[rel="apple-touch-icon"]').forEach(el => document.head.appendChild(el))`
5+
46
describe('app-dir - metadata-icons', () => {
57
const { next } = nextTestSetup({
68
files: __dirname,
@@ -45,15 +47,13 @@ describe('app-dir - metadata-icons', () => {
4547
})
4648

4749
it('should not contain icon insertion script when metadata is rendered in head', async () => {
48-
const iconInsertionScript = `document.querySelectorAll('body link[rel="icon"], body link[rel="apple-touch-icon"]').forEach(el => document.head.appendChild(el))`
49-
5050
const suspendedHtml = await next.render('/custom-icon')
5151
expect(suspendedHtml).toContain(iconInsertionScript)
5252
})
5353

5454
it('should not contain icon replacement mark in html or after hydration', async () => {
5555
const html = await next.render('/custom-icon')
56-
expect(html).not.toContain('<meta name="«nxt-icon»" content=""/>')
56+
expect(html).not.toContain('<meta name="«nxt-icon»">')
5757
expect(html).not.toContain('«nxt-icon»')
5858

5959
const browser = await next.browser('/custom-icon')
@@ -112,4 +112,32 @@ describe('app-dir - metadata-icons', () => {
112112
])
113113
})
114114
})
115+
116+
it('should re-insert the icons into head when icons are inserted in body during initial chunk', async () => {
117+
const $ = await next.render$('/custom-icon/delay-icons')
118+
expect($('meta[name="«nxt-icon»"]').length).toBe(0)
119+
120+
// body will contain the icons and the script to insert them into head
121+
const body = $('body')
122+
const icons = body.find('link[rel="icon"]')
123+
expect(icons.length).toBe(2)
124+
expect(Array.from(icons.map((_, el) => $(el).attr('href')))).toContain(
125+
'/heart.png'
126+
)
127+
128+
const bodyHtml = body.html()
129+
expect(bodyHtml).toContain(iconInsertionScript)
130+
131+
// icons should be inserted into head after hydration
132+
const browser = await next.browser('/custom-icon/delay-icons')
133+
await retry(async () => {
134+
const iconsInHead = await browser.elementsByCss('head link[rel="icon"]')
135+
const iconUrls = await Promise.all(
136+
iconsInHead.map(
137+
async (el) => (await el.getAttribute('href')).split('?')[0]
138+
)
139+
)
140+
expect(iconUrls).toEqual(['/favicon.ico', '/heart.png'])
141+
})
142+
})
115143
})

0 commit comments

Comments
 (0)