Migrating from vue-meta to Unhead
- vue-meta never shipped stable Vue 3 support and was archived in 2025
- Unhead is the modern replacement with full Vue 3 support
- Most APIs translate directly:
metaInfobecomesuseHead/useSeoMeta
If your app uses Vue 2, migrate the framework to Vue 3 before following these Unhead examples.
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 in October 2025 after years without updates. Unhead is its modern replacement, built by the same ecosystem and Vue 3 native from the start.
Quick Comparison
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() replaces nested meta arrays and name/content objects with flat properties.
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
1. Remove vue-meta
npm uninstall vue-meta
npm install @unhead/vue2. Update Plugin Setup
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
<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
| 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
Reactivity Model
vue-meta required metaInfo() as a function for reactivity; Unhead is reactive by default, so pass refs directly:
<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
Unhead v2 removed implicit context. These examples are component-setup excerpts; fetchData is your application's data loader. Don't call useHead() after await without saving the head instance:
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
vue-meta had inject() for SSR. Unhead uses renderSSRHead():
// Excerpt: head is the request-owned instance installed on your SSR app.
import { renderSSRHead } from '@unhead/vue/server'
const { headTags, bodyTags, bodyTagsOpen, htmlAttrs, bodyAttrs } = await renderSSRHead(head)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?
| vue-meta | Unhead |
|---|---|
| Last stable release: 2020 | Actively maintained |
| Vue 2 only | Vue 3 native |
| Limited TypeScript | Full type safety |
| Community abandoned | Official Nuxt ecosystem |
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 for Nuxt-specific setup.
Checklist
- Uninstalled
vue-metaand installed@unhead/vue - Replaced
Vue.use(VueMeta)withcreateHead()andapp.use(head) - Converted every
metaInfo()block touseHead()oruseSeoMeta() - Renamed
vmid/hidfields tokey - Pass refs and computed getters directly to
useHead(), not.value - Call
useHead()before anawait, or useinjectHead()to keep context after async code - Registered
TemplateParamsPlugin()if you use customtitleTemplateparams like%separator
Pre-launch setup, post-launch verification, and ongoing monitoring. Interactive checklist with links to every guide.