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

Skip to content

Commit 5c0b293

Browse files
authored
fix(unhead): deduplicate script[src] and link[href] tags across entries (#668)
Tags without an explicit dedup key (like `script[src]` and `link[href]`) were keyed by their unique position index in `dedupeTags()`, meaning identical tags from separate entries were never deduplicated. Now falls back to `hashTag()` (content-based hash) instead of position, so identical tags collide and deduplicate. The hash is precomputed during tag normalization and cached on `_h` to avoid recomputation in the DOM renderer. Closes #666
1 parent fff5e12 commit 5c0b293

File tree

7 files changed

+60
-3
lines changed

7 files changed

+60
-3
lines changed

packages/unhead/src/client/renderDOMHead.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export async function renderDOMHead<T extends Unhead<any>>(head: T, options: Ren
3838
const count = dupeKeyCounter.get(tag._d!) || 0
3939
const res = {
4040
tag,
41-
id: (count ? `${tag._d}:${count}` : tag._d) || hashTag(tag),
41+
id: (count ? `${tag._d}:${count}` : tag._d) || tag._h!,
4242
shouldRender: true,
4343
}
4444
if (tag._d && isMetaArrayDupeKey(tag._d)) {

packages/unhead/src/unhead.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import type {
1212
} from './types'
1313
import { createHooks } from 'hookable'
1414
import { isMetaArrayDupeKey, sortTags, tagWeight, UsesMergeStrategy, ValidHeadTags } from './utils'
15-
import { dedupeKey } from './utils/dedupe'
15+
import { dedupeKey, hashTag } from './utils/dedupe'
1616
import { normalizeEntryToTags } from './utils/normalize'
1717

1818
function registerPlugin(head: Unhead<any>, p: HeadPluginInput) {
@@ -111,6 +111,8 @@ export function createUnhead<T = ResolvableHead>(resolvedOptions: CreateHeadOpti
111111
t._w = tagWeight(head, t)
112112
t._p = (e._i << 10) + i
113113
t._d = dedupeKey(t)
114+
if (!t._d)
115+
t._h = hashTag(t)
114116
return t
115117
})
116118
}
@@ -120,7 +122,7 @@ export function createUnhead<T = ResolvableHead>(resolvedOptions: CreateHeadOpti
120122
.flatMap(e => (e._tags || []).map(t => ({ ...t, props: { ...t.props } })))
121123
.sort(sortTags)
122124
.reduce((acc, next) => {
123-
const k = String(next._d || next._p)
125+
const k = next._d || next._h!
124126
if (!acc.has(k))
125127
return acc.set(k, next)
126128

packages/unhead/test/unit/client/promises.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ describe('promises', () => {
3131
},
3232
{
3333
"_d": undefined,
34+
"_h": "script:src:https://example.com/script.js",
3435
"_p": 1025,
3536
"_w": 50,
3637
"props": {

packages/unhead/test/unit/client/resolveTags.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ describe('resolveTags', () => {
5858
},
5959
{
6060
"_d": undefined,
61+
"_h": "script:src:https://cdn.example.com/script.js",
6162
"_p": 1026,
6263
"_w": 50,
6364
"props": {
@@ -88,6 +89,7 @@ describe('resolveTags', () => {
8889
},
8990
{
9091
"_d": undefined,
92+
"_h": "link:rel:icon,type:image/x-icon,href:https://cdn.example.com/favicon.ico",
9193
"_p": 1028,
9294
"_w": 100,
9395
"props": {
@@ -122,6 +124,7 @@ describe('resolveTags', () => {
122124
[
123125
{
124126
"_d": undefined,
127+
"_h": "script:src:https://cdn.example.com/script2.js",
125128
"_p": 2048,
126129
"_w": 50,
127130
"props": {
@@ -162,6 +165,7 @@ describe('resolveTags', () => {
162165
},
163166
{
164167
"_d": undefined,
168+
"_h": "script:src:https://cdn.example.com/script2.js",
165169
"_p": 1026,
166170
"_w": 50,
167171
"props": {
@@ -192,6 +196,7 @@ describe('resolveTags', () => {
192196
},
193197
{
194198
"_d": undefined,
199+
"_h": "link:rel:icon,type:image/x-icon,href:https://cdn.example.com/favicon.ico",
195200
"_p": 1028,
196201
"_w": 100,
197202
"props": {

packages/unhead/test/unit/e2e/deduping.test.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,52 @@ describe('unhead e2e deduping', () => {
178178
179179
180180
181+
</body></html>"
182+
`)
183+
})
184+
185+
it('duplicate script src and link href across entries', async () => {
186+
const ssrHead = createClientHeadWithContext()
187+
ssrHead.push({
188+
script: [{ src: 'https://example.com/app.js', async: true }],
189+
link: [{ rel: 'stylesheet', href: 'https://example.com/style.css' }],
190+
})
191+
ssrHead.push({
192+
script: [{ src: 'https://example.com/app.js', async: true }],
193+
link: [{ rel: 'stylesheet', href: 'https://example.com/style.css' }],
194+
})
195+
196+
const data = await renderSSRHead(ssrHead)
197+
expect(data.headTags).toMatchInlineSnapshot(`
198+
"<script src="https://example.com/app.js" async></script>
199+
<link rel="stylesheet" href="https://example.com/style.css">"
200+
`)
201+
202+
const dom = useDom(data)
203+
const csrHead = createClientHeadWithContext()
204+
csrHead.push({
205+
script: [{ src: 'https://example.com/app.js', async: true }],
206+
link: [{ rel: 'stylesheet', href: 'https://example.com/style.css' }],
207+
})
208+
csrHead.push({
209+
script: [{ src: 'https://example.com/app.js', async: true }],
210+
link: [{ rel: 'stylesheet', href: 'https://example.com/style.css' }],
211+
})
212+
await renderDOMHead(csrHead, { document: dom.window.document })
213+
214+
expect(dom.serialize()).toMatchInlineSnapshot(`
215+
"<!DOCTYPE html><html><head>
216+
<script src="https://example.com/app.js" async=""></script>
217+
<link rel="stylesheet" href="https://example.com/style.css">
218+
</head>
219+
<body>
220+
221+
<div>
222+
<h1>hello world</h1>
223+
</div>
224+
225+
226+
181227
</body></html>"
182228
`)
183229
})

packages/unhead/test/unit/server/tagPriority.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ describe('tag priority', () => {
2525
[
2626
{
2727
"_d": undefined,
28+
"_h": "script:src:/very-important-script.js",
2829
"_p": 1024,
2930
"_w": 42,
3031
"props": {
@@ -35,6 +36,7 @@ describe('tag priority', () => {
3536
},
3637
{
3738
"_d": undefined,
39+
"_h": "script:src:/not-important-script.js",
3840
"_p": 2048,
3941
"_w": 50,
4042
"props": {

packages/vue/test/unit/promises.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ describe('vue promises', () => {
3636
},
3737
{
3838
"_d": undefined,
39+
"_h": "script:src:https://example.com/script.js",
3940
"_p": 1025,
4041
"_w": 50,
4142
"props": {

0 commit comments

Comments
 (0)