SEO in Nuxt with @nuxtjs/seo

SEO in Nuxt with @nuxtjs/seo

Set up sitemaps, meta tags, structured data, OG images, and robots.txt in Nuxt with the official SEO module.

Reza Baar

Reza Baar

June 17, 2026

Nuxt already gives you SSR out of the box, which is half the SEO battle. But you still need sitemaps, proper meta tags, structured data, robots.txt, and OG images. The @nuxtjs/seo module bundles six SEO modules into a single install and configures sensible defaults for all of them. In this post, we'll set it up, configure each piece, and cover the newer AI-readiness considerations.

Read more at nuxtseo.com.

Installation

One command installs the entire SEO stack:

      npx nuxi module add @nuxtjs/seo

    

This registers @nuxtjs/seo in your config, which is an alias that installs these seven modules together:

  • @nuxtjs/robots for robots.txt
  • @nuxtjs/sitemap for XML sitemaps
  • nuxt-og-image for dynamic Open Graph images
  • nuxt-schema-org for structured data (JSON-LD)
  • nuxt-seo-utils for canonical URLs and trailing slash normalization
  • nuxt-link-checker for catching broken links during development
  • nuxt-site-config for shared site configuration (also auto-installs with any individual SEO module)

Site Configuration

All SEO modules need your site URL. Set it once in nuxt.config.ts and every module picks it up:

      // nuxt.config.ts
export default defineNuxtConfig({
  site: {
    url: 'https://example.com',
    name: 'My Nuxt App',
    description: 'A Nuxt application with full SEO setup',
    defaultLocale: 'en',
  },
});

    

This single site config feeds the sitemap, canonical URLs, OG images, and structured data. No need to repeat the URL in five different places.

Meta Tags with useSeoMeta

The recommended way to set page-level meta tags is useSeoMeta. It's type-safe and uses a flat object instead of nested arrays:

      <!-- pages/blog/[slug].vue -->
<script setup lang="ts">
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?.description,
  ogTitle: () => post.value?.title,
  ogDescription: () => post.value?.description,
  ogImage: () => post.value?.coverImage,
  ogType: 'article',
  twitterCard: 'summary_large_image',
});
</script>

    

Using getter functions (() => post.value?.title) makes the meta tags reactive. If the data updates, the tags update too.

For a global default across all pages, set it in app.vue or a layout:

      <!-- app.vue -->
<script setup lang="ts">
useSeoMeta({
  titleTemplate: '%s | My Nuxt App',
  ogSiteName: 'My Nuxt App',
  twitterCard: 'summary_large_image',
});
</script>

<template>
  <NuxtLayout>
    <NuxtPage />
  </NuxtLayout>
</template>

    

The titleTemplate inserts each page's title into the pattern. A page with title: 'About' renders as "About | My Nuxt App".

Sitemap

The sitemap module generates an XML sitemap at /sitemap.xml automatically from your pages/ directory and any dynamic routes you configure:

      // nuxt.config.ts
export default defineNuxtConfig({
  sitemap: {
    sources: ['/api/__sitemap__/urls'],
  },
});

    

For dynamic content (blog posts from a CMS), create a server route that returns the URLs:

      // server/api/__sitemap__/urls.ts
export default defineSitemapEventHandler(async () => {
  const posts = await $fetch('/api/posts');

  return posts.map((post) => ({
    loc: `/blog/${post.slug}`,
    lastmod: post.updatedAt,
    changefreq: 'weekly',
    priority: 0.8,
  }));
});

    

The sitemap includes your static pages automatically. The source endpoint adds the dynamic ones.

Robots.txt

The robots module generates /robots.txt from your config:

      // nuxt.config.ts
export default defineNuxtConfig({
  robots: {
    groups: [
      {
        userAgent: '*',
        allow: '/',
        disallow: ['/admin', '/api'],
      },
    ],
    sitemap: 'https://example.com/sitemap.xml',
  },
});

    

It also supports blocking specific AI crawlers if you want to control how LLMs consume your content:

      // nuxt.config.ts
export default defineNuxtConfig({
  robots: {
    groups: [
      {
        userAgent: '*',
        allow: '/',
      },
      {
        userAgent: ['GPTBot', 'ChatGPT-User', 'CCBot'],
        disallow: ['/'],
      },
    ],
  },
});

    

This lets regular search engines crawl your site while blocking AI training crawlers. Whether you want to do this depends on your content strategy.

Structured Data with useSchemaOrg

Schema.org markup helps search engines understand your content. The nuxt-schema-org module provides composables for adding JSON-LD structured data:

      <!-- pages/blog/[slug].vue -->
<script setup lang="ts">
const route = useRoute();
const { data: post } = await useAsyncData(`post-${route.params.slug}`, () =>
  $fetch(`/api/posts/${route.params.slug}`)
);

useSchemaOrg([
  defineArticle({
    headline: post.value?.title,
    description: post.value?.description,
    datePublished: post.value?.publishedAt,
    dateModified: post.value?.updatedAt,
    author: {
      name: post.value?.author.name,
      url: post.value?.author.url,
    },
    image: post.value?.coverImage,
  }),
]);
</script>

    

For your site-wide identity, add organization or person schema in app.vue:

      <!-- app.vue -->
<script setup lang="ts">
useSchemaOrg([
  defineOrganization({
    name: 'My Company',
    url: 'https://example.com',
    logo: 'https://example.com/logo.png',
    sameAs: [
      'https://twitter.com/mycompany',
      'https://github.com/mycompany',
    ],
  }),
  defineWebSite({
    name: 'My Nuxt App',
  }),
]);
</script>

    

The module automatically nests the structured data correctly. A page-level article schema connects to the site-level organization schema without you wiring them together.

Dynamic OG Images

The nuxt-og-image module (v6, shipped with @nuxtjs/seo v5) generates Open Graph images on demand. Instead of creating a PNG for every page, you define a template component and it renders one dynamically:

      <!-- pages/blog/[slug].vue -->
<script setup lang="ts">
defineOgImage('BlogPost', {
  title: post.value?.title,
  description: post.value?.description,
  siteName: 'My Nuxt App',
});
</script>

    

Note: if you're migrating from an older version, defineOgImageComponent was deprecated in v6. The new API is defineOgImage with the component name as the first argument. Run npx nuxt-og-image migrate v6 to auto-migrate.

Create the OG image template as a Vue component in components/OgImage/:

      <!-- components/OgImage/BlogPost.vue -->
<script setup lang="ts">
defineProps<{
  title: string;
  description: string;
  siteName: string;
}>();
</script>

<template>
  <div class="og-image">
    <h1>{{ title }}</h1>
    <p>{{ description }}</p>
    <span>{{ siteName }}</span>
  </div>
</template>

    

The module renders this component as a PNG and serves it at the URL referenced in the OG meta tags. The default renderer is Takumi (2-10x faster than the old Satori renderer) and images can be cached at the edge.

The nuxt-link-checker module scans for broken links during development. You'll see warnings in your terminal when a <NuxtLink> points to a page that doesn't exist:

      [link-checker] /blog/old-post -> 404

    

This catches dead links before they hit production.

Putting It All Together

Here's what a fully configured nuxt.config.ts looks like with all SEO options:

      // nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@nuxtjs/seo'],

  site: {
    url: 'https://example.com',
    name: 'My Nuxt App',
    description: 'A Nuxt application with full SEO setup',
  },

  sitemap: {
    sources: ['/api/__sitemap__/urls'],
  },

  robots: {
    groups: [
      {
        userAgent: '*',
        allow: '/',
        disallow: ['/admin'],
      },
    ],
  },

  ogImage: {
    enabled: true,
  },

  schemaOrg: {
    identity: 'Organization',
  },
});

    

Most of the actual SEO work happens in page-level composables (useSeoMeta, useSchemaOrg, defineOgImageComponent), not in the config file. The config sets the foundation, and each page adds its specifics.

Key Takeaways

  • @nuxtjs/seo bundles six SEO modules into one install with zero-config defaults
  • Set your site URL once in site config and all modules use it
  • useSeoMeta is the recommended way to set meta tags with full TypeScript support
  • Sitemaps auto-generate from pages and can pull dynamic URLs from a server endpoint
  • useSchemaOrg adds structured data that connects page-level and site-level entities
  • OG images can be generated dynamically from Vue component templates
  • Robots.txt supports blocking AI crawlers alongside traditional search engine rules

Conclusion

We set up the full Nuxt SEO stack: meta tags, sitemaps, structured data, OG images, robots.txt, and link checking. The module handles the tedious wiring so each page just needs a few composable calls to be fully optimized for search engines.

More certificates.dev articles

Get the latest news and updates on developer certifications. Content is updated regularly, so please make sure to bookmark this page or sign up to get the latest content directly in your inbox.