useHead() instead of document.title for SSR compatibilityog:title separately for social sharingPage titles appear in browser tabs and as the clickable headline in search results. Google uses your <title> tag over 80% of the time when generating title links—though studies show it rewrites 61-76% of titles to some degree.
<head>
<title>Mastering Titles in Vue · Vue SEO</title>
</head>
Setting titles in Vue 3 requires a head manager like Unhead since document.title won't work during server-side rendering.
// Basic title
useHead({ title: 'Home' })
// With template (adds site name)
useHead({
title: 'Home',
titleTemplate: '%s | MySite'
})
// Reactive title from data
const post = ref({ title: 'Loading...' })
useHead({
title: () => post.value.title
})
// SEO-focused (includes og:title)
useSeoMeta({
title: 'Home',
ogTitle: 'Home | MySite'
})
document.title?You might try setting titles directly:
// ❌ Breaks SSR, may not be indexed
document.title = 'Home'
This fails during server-side rendering—the title won't exist in the initial HTML response. Search engines render JavaScript but may not wait for client-side updates.
document.title in a Vue SPA means search engines may index your pages with missing or incorrect titles. Always use a head manager like Unhead for SEO-critical metadata.Use Unhead instead. It handles both SSR and client-side updates. For Google's official guidance, see Influencing your title links.
useHead()The useHead() composable sets titles that work in SSR and client-side navigation:
<script setup lang="ts">
useHead({
title: 'Home'
})
</script>
<head>
<title>Home</title>
</head>
Works in any component. You can set other head tags in the same call:
<script setup lang="ts">
useHead({
title: 'Home',
meta: [
{ name: 'description', content: 'Welcome to MyApp' }
]
})
</script>
.value.useHead({
title: myTitle.value // ❌ Loses reactivity
})
useHead({
title: myTitle // ✅ Stays reactive
})
Computed getter syntax works for derived titles:
const post = ref({ title: 'Loading...' })
useHead({
title: () => post.value.title // Updates when post changes
})
Fetch data before render, not in onMounted(). Client-only fetches mean search engines see your loading state:
<script setup lang="ts">
const postTitle = ref('Loading...')
useHead({ title: postTitle })
// ❌ onMounted runs after SSR—Google sees "Loading..."
onMounted(async () => {
postTitle.value = (await fetchPostData()).title
})
</script>
Use SSR-compatible data fetching (Vue's onServerPrefetch() or framework solutions like Nuxt's useFetch()).
Most sites append a site name to titles for brand recognition. Google recommends adding your site name with a delimiter:
<head>
<title>Home | MySite</title>
</head>
Use titleTemplate with a title template:
<script setup lang="ts">
useHead({
title: 'Home',
titleTemplate: '%s | MySite'
})
</script>
<head>
<title>Home | MySite</title>
</head>
The %s token gets replaced with your page title (or empty string if none set).
Override the template for specific pages by passing null:
<script lang="ts" setup>
useHead({
title: 'Home',
titleTemplate: null
})
</script>
<head>
<title>Home</title>
</head>
For dynamic site names or separators, use the Template Params plugin:
import { TemplateParamsPlugin } from '@unhead/vue'
const head = injectHead()
head.use(TemplateParamsPlugin)
Then use custom params:
<script setup lang="ts">
useHead({
title: 'Home',
titleTemplate: '%s %separator %siteName',
templateParams: {
separator: '·',
siteName: 'MySite'
}
})
</script>
<head>
<title>Home · MySite</title>
</head>
Common separators: | - — • ·
Template params work in meta tags too:
useHead({
templateParams: { siteName: 'MyApp' },
title: 'Home',
meta: [
{ name: 'description', content: 'Welcome to %siteName' },
{ property: 'og:title', content: 'Home | %siteName' }
]
})
Social platforms use og:title and twitter:title meta tags. Use useSeoMeta() to set these:

<script setup lang="ts">
useSeoMeta({
title: 'Why you should eat more broccoli',
titleTemplate: '%s | Health Tips',
// og:title doesn't use titleTemplate—set it explicitly
ogTitle: 'Health Tips: 10 reasons to eat more broccoli',
// twitter:title only needed if different from og:title
twitterTitle: 'Hey X! 10 reasons to eat more broccoli',
})
</script>
<head>
<title>Why you should eat more broccoli | Health Tips</title>
<meta property="og:title" content="Health Tips: 10 reasons to eat more broccoli" />
<meta name="twitter:title" content="Hey X! 10 reasons to eat more broccoli" />
</head>
Twitter/X falls back to og:title if twitter:title isn't set.
Google displays roughly 50-60 characters before truncating. Studies show titles between 51-60 characters have the lowest rewrite rates (39-42%).
Longer titles still get indexed—Google just truncates the display. Front-load important keywords since users may only see the first 50 characters.
Set titles from route meta for centralized title management:
// router.ts
const routes = [
{ path: '/', component: Home, meta: { title: 'Home' } },
{ path: '/about', component: About, meta: { title: 'About Us' } },
{ path: '/blog/:slug', component: BlogPost, meta: { title: 'Blog' } }
]
Use a navigation guard to apply titles on route change:
router.afterEach((to) => {
const title = to.meta.title as string
if (title) {
useHead({ title })
}
})
For dynamic routes, override in the component:
<script setup lang="ts">
// Fetched post data overrides route meta
const post = await fetchPost(route.params.slug)
useHead({
title: post.title // "How to Build a Blog" instead of generic "Blog"
})
</script>
Set a global title template in your guard:
router.afterEach((to) => {
useHead({
title: to.meta.title as string || 'MySite',
titleTemplate: '%s | MySite'
})
})
Handle nested routes by walking matched routes:
router.afterEach((to) => {
// Find deepest route with a title
const title = [...to.matched]
.reverse()
.find(r => r.meta.title)
?.meta
.title as string
if (title)
useHead({ title })
})
Using Nuxt? Check out Nuxt SEO which handles much of this automatically. Learn more about Page Titles in Nuxt →
Which approach correctly sets a reactive title that updates when data changes?
useHead({ title: myTitle.value }) - Loses reactivityuseHead({ title: myTitle }) - Correct! Stays reactivedocument.title = myTitle.value - Breaks SSRuseHead({ title: () => myTitle }) - Unnecessary wrapperuseHead() or useSeoMeta() instead of document.titleog:title for social sharingMastering Meta
Set up meta tags in Vue 3 with Unhead. Covers titles, descriptions, Open Graph, Twitter Cards, and Schema.org—with SSR patterns that actually get indexed.
Meta Description
Meta descriptions get rewritten by Google 70% of the time anyway. Here's how to implement them properly in Vue using composables and let your content do the heavy lifting.