SEO with useHead and useSeoMeta
Setting per-page titles, descriptions, and social preview tags so every page — not just the homepage — is search- and share-ready.
អាន 2 នាទី
A single static <title> and <meta> set in app.vue or nuxt.config.ts covers the whole site with the same tags — fine for a single-page app, but wrong the moment you have more than one page that ought to describe itself differently in search results and link previews. Nuxt gives you two composables for setting these per page, reactively.
useSeoMeta for the common cases
useSeoMeta covers the tags you'll set on nearly every page — title, description, and Open Graph/Twitter card data for link previews — with plain, typo-checked properties instead of raw tag names:
<!-- pages/blog/[slug].vue -->
<script setup>
const route = useRoute();
const { data: post } = await useFetch(`/api/posts/${route.params.slug}`);
useSeoMeta({
title: post.value.title,
description: post.value.excerpt,
ogTitle: post.value.title,
ogDescription: post.value.excerpt,
ogImage: post.value.coverImage,
twitterCard: "summary_large_image",
});
</script>Because post is reactive data from useFetch, these tags update automatically if the underlying data changes — useful for something like a preview mode where an editor is live-editing the post.
useHead for anything more custom
useHead is the lower-level composable underneath useSeoMeta — reach for it when you need something useSeoMeta doesn't have a named property for: a canonical link, structured data, or a custom <script> tag:
<script setup>
useHead({
link: [{ rel: "canonical", href: `https://example.com/blog/${route.params.slug}` }],
script: [
{
type: "application/ld+json",
innerHTML: JSON.stringify({
"@context": "https://schema.org",
"@type": "BlogPosting",
headline: post.value.title,
}),
},
],
});
</script>Why this has to run on the server
Search engine crawlers and social-media link-preview bots generally don't execute JavaScript the way a browser does — they read the HTML they're served. If page titles and descriptions were only set client-side (say, with a manual document.title = ... inside onMounted), a crawler would see whatever generic tags were in the initial HTML and never the per-page values. Because useHead/useSeoMeta run as part of Nuxt's SSR pipeline, the tags are already present in the HTML the server sends back — no JavaScript execution required to see the right title or preview image.
A sensible default, overridden per page
A good pattern is to set sitewide fallback tags once in nuxt.config.ts (app.head, shown in the config-basics lesson) and let individual pages override them with useSeoMeta only where they have something more specific to say — a blog post, a product page — rather than repeating the same boilerplate call on every single page.