Index of every page on this site: llms.txt
TypeScript
Core Concepts

Tag Deduplication

Unhead deduplicates tags by their semantic identity or an explicit key. Meta tags normally use name, property, or http-equiv as that identity.

Tag deduplication

When implementing head tags across an application hierarchy (layouts, pages, components), you'll often need to override tags. This automatic replacement of duplicate tags is called "deduplication" or "deduping."

Unhead identifies duplicates from each tag's semantic identity.

HTML permits one title element per document. Repeated description metadata is not an HTML syntax error, but it leaves consumers with competing descriptions. Google recommends a page-specific description when one is provided; see its snippet guidance.

Replacement behavior

When you register multiple tags that are considered duplicates, only the most recent one will be used by default. This allows page-level components to override tags defined at the layout level.

Duplicate identities

Unhead uses several strategies to identify duplicate tags:

  • Special singleton tags: base, title, titleTemplate, bodyAttrs, htmlAttrs
  • Specific link types: <link rel="canonical">, one <link rel="alternate"> per hreflang
  • Charset metadata: <meta charset="">
  • Custom keys: Any tag with a matching key attribute
  • Meta tag identifiers: Matching name, property, or http-equiv attributes
  • Link identity: Matching rel + href combination, other attributes are ignored

These are checked in order. Canonical and hreflang alternate links are true singletons, so they dedupe even when tags carry different key values. The generic rel + href rule only applies when a link has no explicit key or id, which is what makes the keyed escape hatch below work.

Example: Meta Description Override

import { useHead } from 'unhead'

// useHead: /docs/head/api/composables/use-head
// Called in a layout component (higher in the hierarchy)
useHead(unheadInstance, {
  meta: [
    {
      name: 'description',
      content: 'my site wide description',
    },
  ],
})

// Called in a page component (lower in the hierarchy)
useHead(unheadInstance, {
  meta: [
    {
      name: 'description',
      content: 'my page description',
    },
  ],
})

// Result in the rendered HTML:
// <meta name="description" content="my page description" />
You can control which tag takes precedence using Tag Priorities to override the default "most recent wins" behavior.

Multiple values for one meta name

Some metadata allows more than one value. Verification tags from multiple services are one example.

Using Content Arrays

For arrayable meta names such as google-site-verification, you can provide an array of values for content:

import { useHead } from 'unhead'

useHead(unheadInstance, {
  meta: [
    {
      name: 'google-site-verification',
      content: [
        'verification-id-1',
        'verification-id-2',
      ]
    },
  ],
})

// Result in HTML:
// <meta name="google-site-verification" content="verification-id-1">
// <meta name="google-site-verification" content="verification-id-2">

Multiple Tags in a Single Entry

Unhead also preserves multiple arrayable meta tags with the same identifier when they're defined within a single useHead() call:

import { useHead } from 'unhead'

useHead(unheadInstance, {
  meta: [
    {
      name: 'google-site-verification',
      content: 'verification-id-1'
    },
    {
      name: 'google-site-verification',
      content: 'verification-id-2'
    },
  ],
})

// Result in HTML:
// <meta name="google-site-verification" content="verification-id-1">
// <meta name="google-site-verification" content="verification-id-2">
Within one entry, Unhead keeps repeated theme-color, google-site-verification, and author tags. It also keeps Open Graph image, audio, video, and alternate locale arrays; article and book author/tag arrays; and deprecated Twitter image arrays. For every other duplicate meta name, the last tag wins.

Link tags dedupe on their rel + href pair; other attributes are not considered. If you intentionally need two links sharing both, such as a preconnect with and without crossorigin, give each a unique key:

import { useHead } from 'unhead'

useHead(unheadInstance, {
  link: [
    { rel: 'preconnect', href: 'https://cdn.example.com', key: 'preconnect' },
    { rel: 'preconnect', href: 'https://cdn.example.com', crossorigin: '', key: 'preconnect-cors' },
  ],
})

// Result in HTML:
// <link rel="preconnect" href="https://cdn.example.com" data-hid="preconnect">
// <link rel="preconnect" href="https://cdn.example.com" crossorigin data-hid="preconnect-cors">

Custom keys

Provide a custom key when two otherwise similar tags need separate identities.

The key identifies a tag within its tag type. A script and a link may use the same key without deduplicating each other. Canonical links, language alternates, charset, viewport, description, keywords, and robots metadata use their semantic identity even when a key is present.

Example: Ensuring Script Uniqueness

import { useHead } from 'unhead'

useHead(unheadInstance, {
  script: [
    {
      src: 'https://example.com/script.js',
      key: 'my-script',
    },
  ]
})

Overriding Tags Using Keys

Use the same key on a later tag of the same type to update it. Tags with matching explicit keys merge their attributes by default while the later tag supplies top-level fields such as textContent and innerHTML. Set tagDuplicateStrategy: 'replace' when you do not want to retain earlier attributes.

Duplicate strategies

By default, a later duplicate replaces the earlier tag.

Default Strategies

  • For most tags: replace; the new tag replaces the old one
  • For attribute tags: merge; htmlAttrs and bodyAttrs retain existing attributes, including class and style
  • For matching explicit keys: Attributes merge by default; use tagDuplicateStrategy: 'replace' for replacement

The tagDuplicateStrategy Property

You can explicitly control this behavior using the tagDuplicateStrategy property:

import { useHead } from 'unhead'

// In a layout file we added a class
useHead(unheadInstance, {
  htmlAttrs: {
    class: 'my-class',
  },
})

// On a specific page, we want to replace instead of merge
useHead(unheadInstance, {
  htmlAttrs: {
    tagDuplicateStrategy: 'replace', // Override the default merge behavior
    class: 'my-new-class',
  },
})

// Result in HTML:
// <html class="my-new-class">

Practical Examples

Removing an entry

Keep the active entry returned by useHead() and call dispose() when that entry is no longer needed:

import { useHead } from 'unhead'

const entry = useHead(unheadInstance, {
  style: [
    {
      key: 'red-bg',
      textContent: 'body { color: red }',
    }
  ]
})

entry.dispose()

Passing an otherwise empty tag with the same key is not a general removal API: keyed element tags carry an internal data-hid attribute and may render as an empty element. For a meta tag, a later duplicate with content: null is omitted during sanitization.

Replacing a style

import { useHead } from 'unhead'

// A layout file sets the background color to red
useHead(unheadInstance, {
  style: [
    {
      key: 'bg-color',
      textContent: 'body { color: red }',
    }
  ]
})

// In a page component, we want to change it to blue
useHead(unheadInstance, {
  style: [
    {
      key: 'bg-color',
      textContent: 'body { color: blue }',
    }
  ]
})

// Result: Only the blue style is rendered

See Also

Did this page help you?