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

  1. 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>
  2. 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>
  3. 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

  • curl of 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.

← Back to Nuxt 3 SEO & Meta Management