Index of every page on this site: llms.txt

How to Set Page Titles in Vue 3

Set dynamic page titles in Vue 3 with useHead. Learn title templates, reactive titles, and SSR patterns that include titles in the initial HTML response.
Harlan Wilton8 mins read Published Updated
What you'll learn
  • Use useHead() instead of document.title for SSR compatibility
  • Add a title template to append your site name consistently
  • Write concise, descriptive titles. Google truncates title links to fit the device width
  • Set og:title separately for social sharing

Page titles appear in browser tabs. Google uses your title and other page signals to choose a search title link. Write a clear title that matches the page. Google explains the sources it uses.

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

Quick Reference

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

// Basic title
useHead({ title: 'Home' })

// With template (adds site name)
useHead({
  title: 'Home',
  titleTemplate: '%s | MySite'
})
</script>
<script setup lang="ts">
import { useHead } from '@unhead/vue'
import { ref } from 'vue'

// Reactive title from data
const post = ref({ title: 'Loading...' })
useHead({
  title: () => post.value.title
})
</script>
<script setup lang="ts">
import { useSeoMeta } from '@unhead/vue'

// SEO-focused (includes og:title)
useSeoMeta({
  title: 'Home',
  ogTitle: 'Home | MySite'
})
</script>

Why Not 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. Google can render JavaScript. Rendering can fail when scripts or resources are unavailable.

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

Setting Titles with useHead()

The useHead() composable sets titles that work in SSR and client-side navigation:

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

useHead({
  title: 'Home'
})
</script>
output.html
<head>
  <title>Home</title>
</head>

You can set other head tags in the same call from any component:

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

useHead({
  title: 'Home',
  meta: [
    { name: 'description', content: 'Welcome to MyApp' }
  ]
})
</script>

Reactive Titles

Unhead accepts refs, reactive objects, and computed values. Pass the reactive reference directly; don't destructure it with .value.
<script setup lang="ts">
import { useHead } from '@unhead/vue'
import { ref } from 'vue'

const myTitle = ref('Home')

// Loses reactivity
useHead({
  title: myTitle.value
})
</script>

Computed getter syntax works for derived titles:

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

const post = ref({ title: 'Loading...' })

useHead({
  title: () => post.value.title // Updates when post changes
})
</script>

SSR and SEO

Fetch data before render, not in onMounted(). Client-only fetches leave the loading state in the initial HTML response:

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

const postTitle = ref('Loading...')
useHead({ title: postTitle })

// onMounted runs after SSR. Initial HTML contains "Loading..."
onMounted(() => {
  (async () => {
    postTitle.value = (await fetchPostData()).title
  })()
})
</script>

Use SSR-compatible data fetching (Vue's onServerPrefetch() or framework solutions like Nuxt's useFetch()).

Title Templates

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">
import { useHead } from '@unhead/vue'

useHead({
  title: 'Home',
  titleTemplate: '%s | MySite'
})
</script>
output.html
<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:

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

useHead({
  title: 'Home',
  titleTemplate: null
})
</script>
output.html
<head>
  <title>Home</title>
</head>

Template Params

For dynamic site names or separators, register the Template Params plugin when creating your head instance:

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

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

Then use custom params:

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

useHead({
  title: 'Home',
  titleTemplate: '%s %separator %siteName',
  templateParams: {
    separator: '·',
    siteName: 'MySite'
  }
})
</script>
output.html
<head>
  <title>Home · MySite</title>
</head>

Common separators: | - . • ·

Template params work in meta tags too:

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

useHead({
  templateParams: { siteName: 'MyApp' },
  title: 'Home',
  meta: [
    { name: 'description', content: 'Welcome to %siteName' },
    { property: 'og:title', content: 'Home | %siteName' }
  ]
})
</script>

Social Share Titles

Social platforms use og:title and twitter:title meta tags. Use useSeoMeta() to set these:

Nuxt X Share
Nuxt X Share
input.vue
<script setup lang="ts">
import { useSeoMeta } from '@unhead/vue'

useSeoMeta({
  title: 'Why you should eat more broccoli',
  titleTemplate: '%s | Health Tips',
  // og:title ignores titleTemplate, set it explicitly
  ogTitle: 'Health Tips: 10 reasons to eat more broccoli',
  // Set X title explicitly when you need a specific preview
  twitterTitle: 'Hey X! 10 reasons to eat more broccoli',
})
</script>
output.html
<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>

Set twitter:title explicitly when the X preview needs a specific title. Test the current preview on X.

Use a title that describes the page. "How to Set Meta Tags in Vue 3" identifies a tutorial. "Vue 3 Meta Tag API Reference" identifies documentation. These titles help readers choose the right page. They do not guarantee an AI citation.

Title Length

Google sets no fixed title character limit. It truncates title links to fit the device width. Two titles of equal length can display differently. Treat 50-60 characters as an editing guide.

A short title can still be rewritten. Google chooses the title link based on the page and its other signals.

A longer title can still help describe the page. Put the main topic first, since Google truncates the displayed title to fit the device width.

Vue Router Integration

Set titles from route meta for centralized title management. These excerpts assume your Vue app has installed both Vue Router and an Unhead instance. Create a fresh app, router, and head for each SSR request:

// 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:

import { useHead } from '@unhead/vue'

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">
import { useHead } from '@unhead/vue'

// Fetched post data overrides route meta
// Excerpt: route and fetchPost come from your application.
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:

import { useHead } from '@unhead/vue'

router.afterEach((to) => {
  useHead({
    title: to.meta.title as string || 'MySite',
    titleTemplate: '%s | MySite'
  })
})

Handle nested routes by walking matched routes:

import { useHead } from '@unhead/vue'

router.afterEach((to) => {
  // Find deepest route with a title
  const title = to.matched.toReversed()
    .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 →

After deploying a title edit, check the live HTML and Google's last crawl separately. Reported title updates typically took 1 to 2 days at the serving stage, with slow cases lasting weeks or months.

Recrawling and reprocessing add separate waits. Google can choose another title. See Google Search timelines.

Checklist

Checklist
  • Use useHead() or useSeoMeta() instead of document.title
  • Set up a title template with your site name
  • Keep titles concise and check their display width
  • Set og:title for social sharing
  • Fetch data server-side for dynamic titles
  • Test titles render correctly in SSR
Was this page helpful?