fix: key head entries by id and ignore meta content when deduping

Any element with an `id` is keyed by it regardless of attribute order, and a
`meta` without one is keyed by its first attribute other than `content`. This
stops differently named meta tags with the same content from overriding each
other and lets repeated meta tags be kept apart with unique ids.

fixes #5362
closes #5363
closes #5379

Co-authored-by: Lazizbek Ergashev <20501725+lazerg@users.noreply.github.com>
Co-authored-by: shamu45678 <220251922@seu.edu.cn>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
pull/4532/merge
Divyansh Singh 3 weeks ago
parent ed03db9a59
commit b18f30680b

@ -0,0 +1,56 @@
import { mergeHead, type HeadConfig } from 'shared/shared'
describe('shared/shared', () => {
describe('mergeHead', () => {
test('replaces meta tags with the same key in place', () => {
expect(
mergeHead(
[
['meta', { property: 'og:image', content: '/site.png' }],
['meta', { name: 'keywords', content: 'site' }]
],
[['meta', { content: '/page.png', property: 'og:image' }]]
)
).toEqual([
['meta', { content: '/page.png', property: 'og:image' }],
['meta', { name: 'keywords', content: 'site' }]
])
})
test('ignores content when keying meta tags', () => {
const head: HeadConfig[] = [
['meta', { content: 'a', name: 'name1' }],
['meta', { content: 'a', name: 'name2' }]
]
expect(mergeHead(head)).toEqual(head)
})
test('keys any element by id regardless of attribute order', () => {
expect(
mergeHead(
[
['meta', { name: 'author', content: 'a', id: 'author-a' }],
['meta', { name: 'author', content: 'b', id: 'author-b' }],
['script', { id: 'sw' }, 'old']
],
[
['meta', { id: 'author-a', name: 'author', content: 'c' }],
['script', { id: 'sw' }, 'new']
]
)
).toEqual([
['meta', { id: 'author-a', name: 'author', content: 'c' }],
['meta', { name: 'author', content: 'b', id: 'author-b' }],
['script', { id: 'sw' }, 'new']
])
})
test('appends elements without a key', () => {
const head: HeadConfig[] = [
['link', { rel: 'stylesheet', href: '/a.css' }],
['link', { rel: 'stylesheet', href: '/a.css' }]
]
expect(mergeHead(head, head)).toEqual([...head, ...head])
})
})
})

@ -63,7 +63,7 @@ description: VitePress
- Type: `HeadConfig[]`
Specify extra head tags to be injected for the current page. Will be appended after head tags injected by site-level config.
Specify extra head tags to be injected for the current page. They are [merged](./site-config#head) with the head tags injected by site-level config.
```yaml
---

@ -248,6 +248,13 @@ type HeadConfig =
| [string, Record<string, string>, string]
```
Head entries from the site config, [locale config](../guide/i18n), [directory-level config](#directory-level-overrides), [frontmatter](./frontmatter-config#head) and [`transformHead`](#transformhead) are merged in that order. A later entry replaces an earlier one with the same key instead of being appended:
- Any element with an `id` attribute is keyed by its `id`.
- A `meta` element without an `id` is keyed by its first attribute other than `content` (e.g. `name`, `property`, `http-equiv`) and that attribute's value.
Other elements are never deduplicated. To render multiple `meta` tags that would share a key, like several `<meta name="author">`, give each of them a unique `id`.
#### Example: Adding a favicon
```ts

@ -198,25 +198,23 @@ function createTitleTemplate(
export function mergeHead(...headArrays: HeadConfig[][]): HeadConfig[] {
const merged: HeadConfig[] = []
const metaKeyMap = new Map<string, number>()
const keyMap = new Map<string, number>()
for (const current of headArrays) {
for (const tag of current) {
const [type, attrs] = tag
const keyAttr = Object.entries(attrs)[0]
const key = getHeadKey(tag)
if (type !== 'meta' || !keyAttr) {
if (key == null) {
merged.push(tag)
continue
}
const key = `${keyAttr[0]}=${keyAttr[1]}`
const existingIndex = metaKeyMap.get(key)
const existingIndex = keyMap.get(key)
if (existingIndex != null) {
merged[existingIndex] = tag // replace existing tag
} else {
metaKeyMap.set(key, merged.length)
keyMap.set(key, merged.length)
merged.push(tag)
}
}
@ -225,6 +223,16 @@ export function mergeHead(...headArrays: HeadConfig[][]): HeadConfig[] {
return merged
}
// any element is keyed by its `id`; a meta tag without one is keyed by its
// first attribute other than `content` (e.g. `name`, `property`, `http-equiv`)
function getHeadKey([type, attrs]: HeadConfig): string | undefined {
if (attrs.id) return `id=${attrs.id}`
if (type !== 'meta') return
for (const name in attrs) {
if (name !== 'content') return `${name}=${attrs[name]}`
}
}
export function sanitizeFileName(name: string): string {
const match = DRIVE_LETTER_REGEX.exec(name)
const driveLetter = match ? match[0] : ''

Loading…
Cancel
Save