--- title: "Migrate to v3" description: "Migrate from Unhead v2 to v3. Covers breaking changes, deprecated compatibility APIs, and their replacements." navigation: title: "v3" --- Unhead v3 changes several core defaults and removes v2 compatibility behavior. This guide covers the breaking changes and the deprecated compatibility APIs that remain temporarily available. ## Automated Migration Checks Add `ValidatePlugin` during the upgrade to report v2 patterns: ```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', } }) ``` The [unified Vite plugin](/docs/head/guides/build-plugins/overview) injects `ValidatePlugin` in development unless `validate` is disabled. 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 ``` :: webpack consumers should use the framework bundler entry: ```diff - import unhead from '@unhead/addons/webpack' + import { Unhead } from '@unhead/vue/bundler' - plugins: [unhead()] + plugins: Unhead().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 call remains the same after the import change. Nuxt users do not configure this plugin directly. --- ## Legacy Property Names 🚦 Impact Level: **High** The current `createHead()` entry points omit `DeprecationsPlugin`. Rename these properties before upgrading; the deprecated plugin is still available for staged migrations. ### `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 | Old Property | New Property | | ------------- | -------------- | | `children` | `innerHTML` | | `hid` | `key` | | `vmid` | `key` | | `body: true` | `tagPosition: 'bodyClose'` | --- ## 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: | Removed Option | Replacement | | --------------- | ------------- | | `canonicalHost` | `host` | | `canonicalUrl` | `path` + `host` | | `position` | `tagPosition` | | `defaultLanguage` | `inLanguage` | | `defaultCurrency` | `currency` | ```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` → Platform `createHead` ```diff - import { createHeadCore } from 'unhead' + import { createHead } from 'unhead/client' - const head = createHeadCore() + const head = createHead() ``` For SSR, import `createHead` from `unhead/server`. The low-level `createUnhead()` API now requires a renderer and is intended for adapter authors. ### `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 is deprecated and scheduled for removal in v4. 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 `init` hook was removed. `dom:renderTag` remains in the type definitions for compatibility but is deprecated and no longer called internally. `dom:rendered` is also deprecated but is still emitted; prefer the `onRendered` entry option for entry-specific work. 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** | Removed Type | Replacement | | ------------- | ------------- | | `Head` | `SerializableHead` | | `ResolvedHead` | `SerializableHead` | | `MergeHead` | Use generics directly | | `MetaFlatInput` | `MetaFlat` | | `ResolvedMetaFlat` | `MetaFlat` | | `RuntimeMode` | Removed (no replacement needed) | ```diff - import type { Head, MetaFlatInput, RuntimeMode } from 'unhead/types' + import type { MetaFlat, SerializableHead } from 'unhead/types' ``` --- ## 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 API Changes - `resolveScriptKey` : Internal utility, no longer exported - `setHeadInjectionHandler` (Vue) : Head injection is handled automatically - `DeprecationsPlugin` and Vue's `resolveUnrefHeadInput` remain available as compatibility helpers. Avoid them in new code. --- ## 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; do not depend on the compatibility plugin // 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 { createHead } from 'unhead/client' + import { createHead } from 'unhead/server' // Server utilities - import { extractUnheadInputFromHtml } from 'unhead/server' + import { parseHtmlForUnheadExtraction } from 'unhead/parser' // Vue - import { createHeadCore, setHeadInjectionHandler } from '@unhead/vue' - import { ... } from '@unhead/vue/legacy' + import { createHead } from '@unhead/vue/client' + import { createHead } from '@unhead/vue/server' ```