---
title: "Migrating from vue-meta to Unhead"
description: "Step-by-step guide to migrate your Vue app from vue-meta to Unhead for Vue 3 compatibility and modern head management."
canonical_url: "https://nuxtseo.com/learn-seo/vue/mastering-meta/migrating-vue-meta"
last_updated: "2026-01-29"
---

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

<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. 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: 2021
    </td>
    
    <td>
      Actively maintained
    </td>
  </tr>
  
  <tr>
    <td>
      Vue 2 only
    </td>
    
    <td>
      Vue 3 native
    </td>
  </tr>
  
  <tr>
    <td>
      30KB+
    </td>
    
    <td>
      ~8KB gzipped
    </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. 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.
