---
title: "Migrating from vue-meta to Unhead"
description: "Migrate from vue-meta to Unhead in Vue 3, with syntax mapping, breaking changes, and search-and-replace patterns."
canonical_url: "https://nuxtseo.com/learn-seo/vue/mastering-meta/migrating-vue-meta"
last_updated: "2026-07-16"
---

<key-takeaways>

- 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`

</key-takeaways>

vue-meta was the standard for Vue 2 head management but never shipped a stable Vue 3 version: it stalled at a `3.0.0-alpha` release. 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 and Vue 3 native from the start.

## Quick Comparison

<code-group>

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

```ts [Unhead (Vue 3)]
import { useSeoMeta } from '@unhead/vue'

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

</code-group>

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

## Syntax Mapping

<table>
<thead>
  <tr>
    <th>
      vue-meta
    </th>
    
    <th>
      Unhead
    </th>
    
    <th>
      Notes
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        metaInfo: {}
      </code>
    </td>
    
    <td>
      <code className="language-ts shiki shiki-themes github-light github-light material-theme-palenight" language="ts" style="">
        <span class="s0YkB">
          useHead
        </span>
        
        <span class="sqjlB">
          (
        </span>
        
        <span class="sx-uw">
          {}
        </span>
        
        <span class="sqjlB">
          )
        </span>
      </code>
    </td>
    
    <td>
      Static object
    </td>
  </tr>
  
  <tr>
    <td>
      <code className="language-ts shiki shiki-themes github-light github-light material-theme-palenight" language="ts" style="">
        <span class="s0YkB">
          metaInfo
        </span>
        
        <span class="sqjlB">
          ()
        </span>
      </code>
    </td>
    
    <td>
      <code className="language-ts shiki shiki-themes github-light github-light material-theme-palenight" language="ts" style="">
        <span class="s0YkB">
          useHead
        </span>
        
        <span class="sqjlB">
          (
        </span>
        
        <span class="sx-uw">
          {}
        </span>
        
        <span class="sqjlB">
          )
        </span>
      </code>
      
       with refs
    </td>
    
    <td>
      Reactive by default
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        title
      </code>
    </td>
    
    <td>
      <code>
        title
      </code>
    </td>
    
    <td>
      Same
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        titleTemplate: '%s - Site'
      </code>
    </td>
    
    <td>
      <code>
        titleTemplate: '%s - Site'
      </code>
    </td>
    
    <td>
      Same
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        meta: [{ name, content }]
      </code>
    </td>
    
    <td>
      <code className="language-ts shiki shiki-themes github-light github-light material-theme-palenight" language="ts" style="">
        <span class="s0YkB">
          useSeoMeta
        </span>
        
        <span class="sqjlB">
          (
        </span>
        
        <span class="sx-uw">
          {
        </span>
        
        <span class="sqVJQ">
          name
        </span>
        
        <span class="sx-uw">
          :
        </span>
        
        <span class="sqjlB">
          value
        </span>
        
        <span class="sx-uw">
          }
        </span>
        
        <span class="sqjlB">
          )
        </span>
      </code>
    </td>
    
    <td>
      Flattened
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        vmid
      </code>
      
       / <code>
        hid
      </code>
    </td>
    
    <td>
      <code>
        key
      </code>
    </td>
    
    <td>
      For deduplication
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        children
      </code>
    </td>
    
    <td>
      <code>
        innerHTML
      </code>
    </td>
    
    <td>
      Script content
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        body: true
      </code>
    </td>
    
    <td>
      <code>
        tagPosition: 'bodyClose'
      </code>
    </td>
    
    <td>
      Script positioning
    </td>
  </tr>
</tbody>
</table>

## Migration Steps

### 1. Remove vue-meta

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

### 2. Update Plugin Setup

<code-group>

```ts [vue-meta (before)]
import Vue from 'vue'
import VueMeta from 'vue-meta'

Vue.use(VueMeta)
```

```ts [Unhead (after)]
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')
```

</code-group>

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

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

### 3. Convert Components

<code-group>

```vue [vue-meta (before)]
<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>
```

```vue [Unhead (after)]
<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>
```

</code-group>

### 4. Search and Replace Patterns

<table>
<thead>
  <tr>
    <th>
      Find
    </th>
    
    <th>
      Replace
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code className="language-ts shiki shiki-themes github-light github-light material-theme-palenight" language="ts" style="">
        <span class="s0YkB">
          metaInfo
        </span>
        
        <span class="sqjlB">
          ()
        </span>
      </code>
      
       or <code>
        metaInfo: {
      </code>
    </td>
    
    <td>
      <code>
        useHead({
      </code>
      
       or <code>
        useSeoMeta({
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code className="language-ts shiki shiki-themes github-light github-light material-theme-palenight" language="ts" style="">
        <span class="swiL2">
          this
        </span>
        
        <span class="sx-uw">
          .
        </span>
        
        <span class="s0YkB">
          $meta
        </span>
        
        <span class="sqjlB">
          ()
        </span>
        
        <span class="sx-uw">
          .
        </span>
        
        <span class="s0YkB">
          refresh
        </span>
        
        <span class="sqjlB">
          ()
        </span>
      </code>
    </td>
    
    <td>
      Remove (automatic)
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        vmid:
      </code>
    </td>
    
    <td>
      <code>
        key:
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        hid:
      </code>
    </td>
    
    <td>
      <code>
        key:
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        { name: 'description', content:
      </code>
    </td>
    
    <td>
      <code>
        description:
      </code>
      
       (in useSeoMeta)
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        { property: 'og:title', content:
      </code>
    </td>
    
    <td>
      <code>
        ogTitle:
      </code>
      
       (in useSeoMeta)
    </td>
  </tr>
</tbody>
</table>

## Breaking Changes

### Reactivity Model

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

<code-group>

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

```vue [✅ Good]
<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>
```

</code-group>

### No Implicit Context After Async

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

<code-group>

```ts [❌ Bad]
import { useHead } from '@unhead/vue'

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

```ts [✅ Good]
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 })
}
```

</code-group>

### SSR Rendering

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

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

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

### Template Params Plugin

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

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

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

## Why Migrate?

<table>
<thead>
  <tr>
    <th>
      vue-meta
    </th>
    
    <th>
      Unhead
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      Last stable release: 2020
    </td>
    
    <td>
      Actively maintained
    </td>
  </tr>
  
  <tr>
    <td>
      Vue 2 only
    </td>
    
    <td>
      Vue 3 native
    </td>
  </tr>
  
  <tr>
    <td>
      Limited TypeScript
    </td>
    
    <td>
      Full type safety
    </td>
  </tr>
  
  <tr>
    <td>
      Community abandoned
    </td>
    
    <td>
      Official Nuxt ecosystem
    </td>
  </tr>
</tbody>
</table>

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

If you're using Nuxt, you don't need to install Unhead separately. It's built in. See [Nuxt SEO](/docs/nuxt-seo/getting-started/introduction) for Nuxt-specific setup.

## Checklist

<checklist id="vue-meta-migration">

- Uninstalled `vue-meta` and installed `@unhead/vue`
- Replaced `Vue.use(VueMeta)` with `createHead()` and `app.use(head)`
- Converted every `metaInfo()` block to `useHead()` or `useSeoMeta()`
- Renamed `vmid`/`hid` fields to `key`
- Pass refs and computed getters directly to `useHead()`, not `.value`
- Call `useHead()` before an `await`, or use `injectHead()` to keep context after async code
- Registered `TemplateParamsPlugin()` if you use custom `titleTemplate` params like `%separator`

</checklist>
