Migrating from vue-meta to Unhead · Nuxt SEO

-
-
-
-

[1.4K](https://github.com/harlan-zw/nuxt-seo)

[Nuxt SEO on GitHub](https://github.com/harlan-zw/nuxt-seo)

Learn SEO

Master search optimization

Nuxt

 Vue

-
-
-
-
-
-
-

-
-
-
-
-
-
-

-
-
-

-
-
-
-
-
-
-
-
-
-
-

-
-
-

-
-
-
-
-
-
-
-
-

1.
2.
3.
4.
5.

# Migrating from vue-meta to Unhead

Step-by-step guide to migrate your Vue app from vue-meta to Unhead for Vue 3 compatibility and modern head management.

[![Harlan Wilton](https://avatars.githubusercontent.com/u/5326365?v=4)Harlan Wilton](https://x.com/harlan-zw)6 mins read Published Dec 17, 2025 Updated Jan 29, 2026

What you'll learn

- vue-meta never shipped Vue 3 support and was archived in 2025
- Unhead is the modern replacement with full Vue 3 support
- Most APIs translate directly. `metaInfo` becomes `useHead`/`useSeoMeta`

vue-meta was the standard for Vue 2 head management but never shipped a stable Vue 3 version. The [repository was archived](https://github.com/nuxt/vue-meta) in October 2025 after years without updates. [Unhead](https://unhead.unjs.io/) is its modern replacement. built by the same ecosystem, actively maintained, and Vue 3 native.

## [Quick Comparison](#quick-comparison)

vue-meta (Vue 2)

Unhead (Vue 3)

```
export default {
  metaInfo() {
    return {
      title: 'My Page',
      meta: [
        { name: 'description', content: 'Page description' }
      ]
    }
  }
}
```

```
import { useSeoMeta } from '@unhead/vue'

useSeoMeta({
  title: 'My Page',
  description: 'Page description'
})
```

Unhead's `useSeoMeta()` flattens the nested structure. No more `meta` arrays with `name`/`content` objects.

## [Syntax Mapping](#syntax-mapping)

| vue-meta | Unhead | Notes |
| --- | --- | --- |
| `metaInfo: {}` | `useHead({})` | Static object |
| `metaInfo()` | `useHead({})` with refs | Reactive by default |
| `title` | `title` | Same |
| `titleTemplate: '%s - Site'` | `titleTemplate: '%s - Site'` | Same |
| `meta: [{ name, content }]` | `useSeoMeta({ name: value})` | Flattened |
| `vmid` / `hid` | `key` | For deduplication |
| `children` | `innerHTML` | Script content |
| `body: true` | `tagPosition: 'bodyClose'` | Script positioning |

## [Migration Steps](#migration-steps)

### [1. Remove vue-meta](#_1-remove-vue-meta)

```
npm uninstall vue-meta
npm install @unhead/vue
```

### [2. Update Plugin Setup](#_2-update-plugin-setup)

vue-meta (before)

Unhead (after)

```
import Vue from 'vue'
import VueMeta from 'vue-meta'

Vue.use(VueMeta)
```

```
import { createHead } from '@unhead/vue/client'
import { createApp } from 'vue'
import App from './App.vue'

const app = createApp(App)
const head = createHead()
app.use(head)
app.mount('#app')
```

For SSR apps, import from `@unhead/vue/server` instead:

```
import { createHead } from '@unhead/vue/server'
```

### [3. Convert Components](#_3-convert-components)

vue-meta (before)

Unhead (after)

```
<script lang="ts">
export default {
  data() {
    return { pageTitle: 'About Us' }
  },
  metaInfo() {
    return {
      title: this.pageTitle,
      titleTemplate: '%s | MySite',
      meta: [
        { name: 'description', content: 'About our company' },
        { property: 'og:title', content: this.pageTitle },
        { property: 'og:description', content: 'About our company' }
      ]
    }
  }
}
</script>
```

```
<script setup lang="ts">
import { useHead, useSeoMeta } from '@unhead/vue'
import { ref } from 'vue'

const pageTitle = ref('About Us')

useHead({
  title: pageTitle,
  titleTemplate: '%s | MySite'
})

useSeoMeta({
  description: 'About our company',
  ogTitle: pageTitle,
  ogDescription: 'About our company'
})
</script>
```

### [4. Search and Replace Patterns](#_4-search-and-replace-patterns)

| Find | Replace |
| --- | --- |
| `metaInfo()` or `metaInfo: {` | `useHead({` or `useSeoMeta({` |
| `this.$meta().refresh()` | Remove (automatic) |
| `vmid:` | `key:` |
| `hid:` | `key:` |
| `{ name: 'description', content:` | `description:` (in useSeoMeta) |
| `{ property: 'og:title', content:` | `ogTitle:` (in useSeoMeta) |

## [Breaking Changes](#breaking-changes)

### [Reactivity Model](#reactivity-model)

vue-meta required `metaInfo()` as a function for reactivity. Unhead is reactive by default. pass refs directly:

❌ Bad

✅ Good

```
<script lang="ts">
// vue-meta: function required for reactivity
export default {
  metaInfo() {
    return { title: this.dynamicTitle }
  }
}
</script>
```

```
<script setup lang="ts">
import { useHead } from '@unhead/vue'
import { ref } from 'vue'

// Unhead: refs work automatically
const dynamicTitle = ref('Loading...')
useHead({ title: dynamicTitle })
</script>
```

### [No Implicit Context After Async](#no-implicit-context-after-async)

Unhead v2 removed implicit context. Don't call `useHead()` after `await` without saving the head instance:

❌ Bad

✅ Good

```
import { useHead } from '@unhead/vue'

// May fail in Unhead v2
async function loadData() {
  const data = await fetchData()
  useHead({ title: data.title }) // Context lost
}
```

```
import { injectHead } from '@unhead/vue'

// Call before await or use injectHead()
const head = injectHead()
async function loadData() {
  const data = await fetchData()
  head.push({ title: data.title })
}
```

### [SSR Rendering](#ssr-rendering)

vue-meta had `inject()` for SSR. Unhead uses `renderSSRHead()`:

```
// Server entry
import { renderSSRHead } from '@unhead/ssr'

const { headTags, bodyTags, bodyTagsOpen, htmlAttrs, bodyAttrs } = await renderSSRHead(head)
```

### [Template Params Plugin](#template-params-plugin)

`titleTemplate` params like `%separator` require explicit plugin registration in Unhead v2:

```
import { createHead } from '@unhead/vue/client'
import { TemplateParamsPlugin } from '@unhead/vue/plugins'

const head = createHead({
  plugins: [TemplateParamsPlugin]
})
```

## [Why Migrate?](#why-migrate)

| vue-meta | Unhead |
| --- | --- |
| Last stable: 2021 | Actively maintained |
| Vue 2 only | Vue 3 native |
| 30KB+ | ~8KB gzipped |
| Limited TypeScript | Full type safety |
| Community abandoned | Official Nuxt ecosystem |

Unhead also provides `useSeoMeta()` with autocomplete for all SEO properties. no more guessing meta tag names.

If you're using Nuxt, you don't need to install Unhead separately. It's built in. See

 for Nuxt-specific setup.

---

On this page

- [Quick Comparison](#quick-comparison)
- [Syntax Mapping](#syntax-mapping)
- [Migration Steps](#migration-steps)
- [Breaking Changes](#breaking-changes)
- [Why Migrate?](#why-migrate)

[GitHub](https://github.com/harlan-zw/nuxt-seo) [ Discord](https://discord.com/invite/275MBUBvgP)

###

-
-

Modules

-
-
-
-
-
-
-
-
-

###

-
-
-

###

Nuxt

-
-
-
-
-

Vue

-
-
-
-
-
-
-
-

###

-
-
-
-
-
-
-
-
-
-

Copyright © 2023-2026 Harlan Wilton - [MIT License](https://github.com/harlan-zw/nuxt-seo/blob/main/license) · [mdream](https://mdream.dev)