---
title: "Migrate to v3"
description: "Migrate from Unhead v2 to v3. Covers all breaking changes, removed APIs, and their replacements."
canonical_url: "https://unhead.unjs.io/docs/migration-guide/v3"
last_updated: "2026-05-22T16:41:56.951Z"
---

Unhead v3 removes all deprecated APIs and focuses on performance improvements. This guide covers the breaking changes.

## Automated Migration Checks

Add `ValidatePlugin` during your upgrade to automatically detect v2 patterns and get actionable warnings:

```ts
import { ValidatePlugin } from 'unhead/plugins'

const head = createHead({
  plugins: [
    ValidatePlugin() // Detects deprecated props, missing plugins, and more
  ]
})
```

The plugin will warn you about:

- **Missing TemplateParamsPlugin** : template params like `%siteName` are now opt-in and will appear literally without the plugin
- **Missing AliasSortingPlugin** : `before:`/`after:` tag priorities are now opt-in and will be silently ignored without the plugin
- **Deprecated property names** : `children`, `hid`, `vmid`, `body: true` are no longer auto-converted
- **Removed mode option** : `{ mode: 'server' }` on `head.push()` is silently ignored

All rules use ESLint-style config and can be individually disabled:

```ts
ValidatePlugin({
  rules: {
    'missing-template-params-plugin': 'off',
  }
})
```

If you're using the [unified Vite plugin](/docs/head/guides/build-plugins/overview), `ValidatePlugin` is automatically injected in dev so warnings surface in your browser console without any manual setup.

Remove `ValidatePlugin` once your migration is complete, or keep it for ongoing validation.

---

## `@unhead/addons` → `@unhead/bundler`

🚦 Impact Level: **High** (only if you import build plugins manually)

The `@unhead/addons` package has been renamed to `@unhead/bundler`. The old package still works as a deprecation shim that re-exports from `@unhead/bundler`, but logs a runtime warning.

```diff
- pnpm add -D @unhead/addons
+ pnpm add -D @unhead/bundler
```

The default export has also been replaced with a named `Unhead` export:

```diff
- import unhead from '@unhead/addons/vite'
+ import { Unhead } from '@unhead/bundler/vite'

export default defineConfig({
- plugins: [unhead()],
+ plugins: [Unhead()],
})
```

<tip>

Most users should import from their framework's vite subpath instead, which forwards to `@unhead/bundler` and wires up framework-specific runtime plugins:

```ts
import { Unhead } from '@unhead/vue/vite'
// or @unhead/react/vite, @unhead/svelte/vite, @unhead/solid-js/vite
```

</tip>

Webpack consumers should update similarly:

```diff
- import unhead from '@unhead/addons/webpack'
+ import { Unhead } from '@unhead/bundler/webpack'
```

The minify backend subpaths have moved too:

```diff
- import { createJSMinifier } from '@unhead/addons/minify/rolldown'
- import { createCSSMinifier } from '@unhead/addons/minify/lightningcss'
+ import { createJSMinifier } from '@unhead/bundler/minify/rolldown'
+ import { createCSSMinifier } from '@unhead/bundler/minify/lightningcss'
```

See the [Build Plugins overview](/docs/head/guides/build-plugins/overview) for the new options table.

---

## Framework Vite Plugins: Named `Unhead` Export

🚦 Impact Level: **High**

Every framework Vite plugin now exports a named `Unhead` symbol instead of a default export. Update your `vite.config.ts`:

```diff
// Vue
- import unhead from '@unhead/vue/vite'
+ import { Unhead } from '@unhead/vue/vite'

// React
- import unhead from '@unhead/react/vite'
+ import { Unhead } from '@unhead/react/vite'

// Svelte
- import unhead from '@unhead/svelte/vite'
+ import { Unhead } from '@unhead/svelte/vite'

// Solid
- import unhead from '@unhead/solid-js/vite'
+ import { Unhead } from '@unhead/solid-js/vite'

export default defineConfig({
- plugins: [unhead()],
+ plugins: [Unhead()],
})
```

The plugin behaviour is otherwise unchanged. Nuxt users do not need to update anything.

---

## Legacy Property Names

🚦 Impact Level: **High**

The `DeprecationsPlugin` that automatically converted legacy property names has been removed. You must update your head entries to use the current property names.

### `children` → `innerHTML`

```diff
useHead({
  script: [{
-   children: 'console.log("hello")',
+   innerHTML: 'console.log("hello")',
  }]
})
```

### `hid` / `vmid` → `key`

```diff
useHead({
  meta: [{
-   hid: 'description',
+   key: 'description',
    name: 'description',
    content: 'My description'
  }]
})
```

```diff
useHead({
  meta: [{
-   vmid: 'og:title',
+   key: 'og:title',
    property: 'og:title',
    content: 'My Title'
  }]
})
```

### `body: true` → `tagPosition: 'bodyClose'`

```diff
useHead({
  script: [{
    src: '/script.js',
-   body: true,
+   tagPosition: 'bodyClose',
  }]
})
```

### Quick Reference

<table>
<thead>
  <tr>
    <th>
      Old Property
    </th>
    
    <th>
      New Property
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        children
      </code>
    </td>
    
    <td>
      <code>
        innerHTML
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        hid
      </code>
    </td>
    
    <td>
      <code>
        key
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        vmid
      </code>
    </td>
    
    <td>
      <code>
        key
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        body: true
      </code>
    </td>
    
    <td>
      <code>
        tagPosition: 'bodyClose'
      </code>
    </td>
  </tr>
</tbody>
</table>

---

## Schema.org Plugin

🚦 Impact Level: **High**

The `PluginSchemaOrg` and `SchemaOrgUnheadPlugin` exports have been removed. Use `UnheadSchemaOrg` instead.

```diff
- import { PluginSchemaOrg } from '@unhead/schema-org'
+ import { UnheadSchemaOrg } from '@unhead/schema-org'

const head = createHead({
  plugins: [
-   PluginSchemaOrg()
+   UnheadSchemaOrg()
  ]
})
```

For Vue users:

```diff
- import { PluginSchemaOrg } from '@unhead/schema-org/vue'
+ import { UnheadSchemaOrg } from '@unhead/schema-org/vue'
```

### Schema.org Config Options

The following config options have been removed:

<table>
<thead>
  <tr>
    <th>
      Removed Option
    </th>
    
    <th>
      Replacement
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        canonicalHost
      </code>
    </td>
    
    <td>
      <code>
        host
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        canonicalUrl
      </code>
    </td>
    
    <td>
      <code>
        path
      </code>
      
       + <code>
        host
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        position
      </code>
    </td>
    
    <td>
      Use <code>
        tagPosition
      </code>
      
       on individual schema entries
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        defaultLanguage
      </code>
    </td>
    
    <td>
      Use <code>
        inLanguage
      </code>
      
       on schema nodes
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        defaultCurrency
      </code>
    </td>
    
    <td>
      Use <code>
        priceCurrency
      </code>
      
       on schema nodes
    </td>
  </tr>
</tbody>
</table>

```diff
UnheadSchemaOrg({
- canonicalHost: 'https://example.com',
- canonicalUrl: 'https://example.com/page',
+ host: 'https://example.com',
+ path: '/page',
})
```

---

## Server Composables Removed

🚦 Impact Level: **Medium-High**

The `useServerHead`, `useServerHeadSafe`, and `useServerSeoMeta` composables have been removed. Use the standard composables instead.

```diff
- import { useServerHead, useServerSeoMeta } from 'unhead'
+ import { useHead, useSeoMeta } from 'unhead'

- useServerHead({ title: 'My Page' })
+ useHead({ title: 'My Page' })

- useServerSeoMeta({ description: 'My description' })
+ useSeoMeta({ description: 'My description' })
```

If you need server-only head management, use conditional logic:

```ts
if (import.meta.server) {
  useHead({ title: 'Server Only' })
}
```

---

## Core API Changes

🚦 Impact Level: **Medium**

### `createHeadCore` → `createUnhead`

```diff
- import { createHeadCore } from 'unhead'
+ import { createUnhead } from 'unhead'

- const head = createHeadCore()
+ const head = createUnhead()
```

### `headEntries()` → `entries` Map

```diff
- const entries = head.headEntries()
+ const entries = [...head.entries.values()]
```

### `mode` Option Removed

The `mode` option on head entries has been removed. Runtime mode detection is no longer supported.

```diff
head.push({
  title: 'My Page',
- }, { mode: 'server' })
+ })
```

Use the appropriate `createHead` function instead:

```ts
// Client-side
import { createHead } from 'unhead/client'

// Server-side
import { createHead } from 'unhead/server'
```

---

## Vue Legacy Exports

🚦 Impact Level: **Medium**

### `/legacy` Export Path Deprecated

The `@unhead/vue/legacy` import still works but emits a runtime deprecation warning. Update to the explicit client or server import:

```diff
- import { createHead } from '@unhead/vue/legacy'
+ import { createHead } from '@unhead/vue/client'
// or for SSR
+ import { createHead } from '@unhead/vue/server'
```

### `createHeadCore` Removed

```diff
- import { createHeadCore } from '@unhead/vue'
+ import { createHead } from '@unhead/vue/server'
// or for client
+ import { createHead } from '@unhead/vue/client'
```

---

## Server Utilities

🚦 Impact Level: **Low**

### `extractUnheadInputFromHtml` → `parseHtmlForUnheadExtraction`

The function has been moved from `unhead/server` to `unhead/parser`.

```diff
- import { extractUnheadInputFromHtml } from 'unhead/server'
+ import { parseHtmlForUnheadExtraction } from 'unhead/parser'

- const { input } = extractUnheadInputFromHtml(html)
+ const { input } = parseHtmlForUnheadExtraction(html)
```

---

## Hooks

🚦 Impact Level: **Low**

The following hooks have been removed:

- `init` : No longer needed
- `dom:renderTag` : DOM rendering is now synchronous
- `dom:rendered` : Use the `onRendered` option on `useHead()` instead

The `dom:beforeRender` hook is now synchronous and `renderDOMHead` no longer returns a Promise:

```diff
- await renderDOMHead(head, { document })
+ renderDOMHead(head, { document })
```

The SSR hooks (`ssr:beforeRender`, `ssr:render`, `ssr:rendered`) are now synchronous and `renderSSRHead` no longer returns a Promise:

```diff
- const head = await renderSSRHead(head)
+ const head = renderSSRHead(head)
```

---

## Type Changes

🚦 Impact Level: **Low**

<table>
<thead>
  <tr>
    <th>
      Removed Type
    </th>
    
    <th>
      Replacement
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        Head
      </code>
    </td>
    
    <td>
      <code>
        HeadTag
      </code>
      
       or specific tag types
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        ResolvedHead
      </code>
    </td>
    
    <td>
      <code>
        ResolvedHeadTag
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        MergeHead
      </code>
    </td>
    
    <td>
      Use generics directly
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        MetaFlatInput
      </code>
    </td>
    
    <td>
      <code>
        MetaFlat
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        ResolvedMetaFlat
      </code>
    </td>
    
    <td>
      <code>
        MetaFlat
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        RuntimeMode
      </code>
    </td>
    
    <td>
      Removed (no replacement needed)
    </td>
  </tr>
</tbody>
</table>

```diff
- import type { Head, MetaFlatInput, RuntimeMode } from 'unhead'
+ import type { HeadTag, MetaFlat } from 'unhead'
```

---

## Strict Type Narrowing for Link, Script, and Meta

🚦 Impact Level: **Medium**

The `Link` and `Script` types are now strict discriminated unions. Known `rel` and `type` values enforce per-tag required properties at the type level. Use the new `defineLink` and `defineScript` helpers to declare custom values without losing strictness on known ones.

### Link Tags

Known `rel` values now enforce their required properties. For example, preloading a font requires `crossorigin`:

```diff
useHead({
  link: [{
    rel: 'preload',
    as: 'font',
    href: '/font.woff2',
+   crossorigin: 'anonymous', // now required for font preloads
  }]
})
```

For non-standard `rel` values not covered by `KnownLinkRel` (e.g. OpenID endpoints, RSD links), use `defineLink`:

```ts
import { defineLink, useHead } from 'unhead'

useHead({
  link: [
    defineLink({ rel: 'openid2.provider', href: 'https://example.com/openid' }),
  ]
})
```

### Script Tags

Inline scripts must have `textContent` or `innerHTML` and cannot include `src`, `async`, or `defer`. For custom `type` values, use `defineScript`:

```ts
import { defineScript, useHead } from 'unhead'

useHead({
  script: [
    defineScript({ type: 'text/plain', textContent: '...' }),
  ]
})
```

### Meta Content Required

Meta `content` is now required on name, property, and http-equiv meta tags. Use `null` explicitly to remove a meta tag:

```diff
- useHead({ meta: [{ name: 'description' }] }) // no longer valid
+ useHead({ meta: [{ name: 'description', content: null }] }) // removes the tag
```

### String Variables

When `rel` or `type` comes from a variable typed as `string`, TypeScript cannot narrow the union. Wrap it with `defineLink` / `defineScript` or use `as const` for literals:

```ts
import { defineLink, useHead } from 'unhead'

const rel = getRelFromConfig() // string, not a literal
useHead({
  link: [defineLink({ rel, href: '/path' })]
})

// or use as const for literals
const link = { rel: 'canonical' as const, href: '/path' }
useHead({ link: [link] })
```

---

## Other Removed APIs

- `resolveScriptKey` : Internal utility, no longer exported
- `DeprecationsPlugin` : Update property names directly instead
- `resolveUnrefHeadInput` (Vue) : Reactive resolution now happens automatically
- `setHeadInjectionHandler` (Vue) : Head injection is handled automatically

---

## Quick Reference: Import Changes

```diff
// Build plugins
- import unhead from '@unhead/addons/vite'
+ import { Unhead } from '@unhead/bundler/vite'
// or, recommended, from your framework subpath:
+ import { Unhead } from '@unhead/vue/vite'

// Legacy properties - update property names directly, no plugin needed
- import { DeprecationsPlugin } from 'unhead/plugins'

// Schema.org
- import { PluginSchemaOrg, SchemaOrgUnheadPlugin } from '@unhead/schema-org'
+ import { UnheadSchemaOrg } from '@unhead/schema-org'

// Server composables
- import { useServerHead, useServerHeadSafe, useServerSeoMeta } from 'unhead'
+ import { useHead, useHeadSafe, useSeoMeta } from 'unhead'

// Core
- import { createHeadCore } from 'unhead'
+ import { createUnhead } from 'unhead'

// Server utilities
- import { extractUnheadInputFromHtml } from 'unhead/server'
+ import { parseHtmlForUnheadExtraction } from 'unhead/parser'

// Vue
- import { createHeadCore, resolveUnrefHeadInput, setHeadInjectionHandler } from '@unhead/vue'
- import { ... } from '@unhead/vue/legacy'
+ import { createHead } from '@unhead/vue/client'
+ import { createHead } from '@unhead/vue/server'
```
