Dynamic Meta Tags with useHead in Nuxt 3
The point of dynamic metadata is that titles, social tags, and structured data come from the same data that renders the page — and in Nuxt 3 the trick is making sure that data is resolved before the server renders the head. Done right, useSeoMeta and useHead emit complete, correct metadata into the response. Done wrong — data fetched too late or bound non-reactively — the head renders empty. This guide is the data-driven implementation under Nuxt 3 SEO and meta management.
Step-by-step fix
-
Await the route data so it exists at render time.
<script setup> const route = useRoute(); // ✅ awaited: available during SSR, so the head can use it const { data: post } = await useAsyncData(`post-${route.params.slug}`, () => $fetch(`/api/posts/${route.params.slug}`)); </script> -
Bind metadata reactively with getter functions. Passing values directly can capture them before they resolve; getters track the data.
<script setup> useSeoMeta({ title: () => post.value.title, // ✅ getter tracks data description: () => post.value.summary, ogTitle: () => post.value.title, ogImage: () => post.value.ogImage, twitterCard: 'summary_large_image', }); </script><script setup> // ❌ Non-reactive: may serialize before the fetch resolves useSeoMeta({ title: post.value?.title }); </script> -
Add JSON-LD and canonical with
useHead.<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': 'Article', headline: post.value.title, datePublished: post.value.date, }), }], }); </script>
Validation
curlof the route shows the resolved title, OG tags, and JSON-LD in the response.- GSC URL Inspection rendered HTML matches the live head.
- Rich Results Test validates the Article JSON-LD.
- Social debuggers show the correct OG image and title.
Reference
<script setup>
const route = useRoute();
const { data: post } = await useAsyncData(`post-${route.params.slug}`,
() => $fetch(`/api/posts/${route.params.slug}`));
useSeoMeta({
title: () => post.value.title,
description: () => post.value.summary,
ogTitle: () => post.value.title,
ogDescription: () => post.value.summary,
ogImage: () => post.value.ogImage,
});
useHead({
link: [{ rel: 'canonical', href: `https://example.com/blog/${route.params.slug}` }],
});
</script>
Frequently Asked Questions
Why are my Nuxt meta tags empty in the page source? The data the tags depend on was not awaited before rendering, so the server rendered the head before the values existed. Use await with useFetch or useAsyncData and pass getter functions to useSeoMeta so the tags resolve during SSR.
How do I add JSON-LD structured data in Nuxt 3? Use useHead with a script entry of type application/ld+json and your serialized structured data. Because it runs during SSR, the JSON-LD is present in the server response for crawlers to parse.
Related
- Nuxt 3 SEO & Meta Management — the full Nuxt SEO surface.
- JSON-LD Implementation in Single Page Apps — structured data patterns.
- Canonical URL Management in SPAs — canonical handling for Nuxt routes.
← Back to Nuxt 3 SEO & Meta Management