---
name: "Nuxt SEO"
repo: https://github.com/onmax/nuxt-skills
source: https://github.com/onmax/nuxt-skills/blob/HEAD/skills/nuxt-seo/SKILL.md
license: not specified
install: "npx skills add onmax/nuxt-skills --skill nuxt-seo"
page: https://100xlab.com/resources/agent-skills/onmax-nuxt-skills--nuxt-seo--1ljq12k
---

# Nuxt SEO

```bash
npx nuxi module add @nuxtjs/seo
```

## When to Use

Working with:

- SEO configuration (site URL, name, indexability)
- Robots.txt and sitemap.xml generation
- Dynamic OG image generation
- JSON-LD structured data (schema.org)
- Breadcrumbs and canonical URLs

## Loading Files

**Consider loading these reference files based on your task:**

- [ ] [references/site-config.md](references/site-config.md) - if configuring site URL, name, or SEO foundation
- [ ] [references/crawlability.md](references/crawlability.md) - if setting up robots.txt or sitemap.xml
- [ ] [references/og-image.md](references/og-image.md) - if generating dynamic OG images
- [ ] [references/schema-org.md](references/schema-org.md) - if adding JSON-LD structured data
- [ ] [references/utilities.md](references/utilities.md) - if working with breadcrumbs, canonical URLs, or link checking

**DO NOT load all files at once.** Load only what's relevant to your current task.

## Site Config

Foundation for all SEO modules. Configure `site` in `nuxt.config.ts`, access via `useSiteConfig()`. See [references/site-config.md](references/site-config.md) for full options.

## Module Overview

| Module            | Purpose         | Key API                       |
| ----------------- | --------------- | ----------------------------- |
| nuxt-site-config  | Shared config   | `useSiteConfig()`             |
| @nuxtjs/robots    | robots.txt      | `useRobotsRule()`             |
| @nuxtjs/sitemap   | sitemap.xml     | `defineSitemapEventHandler()` |
| nuxt-og-image     | OG images       | `defineOgImage()`             |
| nuxt-schema-org   | JSON-LD         | `useSchemaOrg()`              |
| nuxt-seo-utils    | Meta utilities  | `useBreadcrumbItems()`        |
| nuxt-link-checker | Link validation | Build-time checks             |

## Nuxt Content v3

Use `asSeoCollection()` for automatic sitemap, og-image, and schema-org from frontmatter:

```ts
// content.config.ts
import { defineCollection, defineContentConfig } from '@nuxt/content'
import { asSeoCollection } from '@nuxtjs/seo/content'

export default defineContentConfig({
  collections: {
    posts: defineCollection(asSeoCollection({ type: 'page', source: 'posts/**' }))
  }
})
```

**Important:** Load `@nuxtjs/seo` before `@nuxt/content` in modules array:

```ts
export default defineNuxtConfig({
  modules: ['@nuxtjs/seo', '@nuxt/content']
})
```

Frontmatter fields: `ogImage`, `sitemap`, `robots`, `schemaOrg`.

## Related Skills

- [nuxt-content](../nuxt-content/SKILL.md) - For MDC rendering with SEO frontmatter

## Links

- [Documentation](https://nuxtseo.com)
- [GitHub](https://github.com/harlan-zw/nuxt-seo)

## Token Efficiency

Main skill: ~250 tokens. Each sub-file: ~400-600 tokens. Only load files relevant to current task.

---

## Supporting file: references/crawlability.md

# Crawlability: Robots & Sitemap

## Robots.txt

Auto-generated at `/robots.txt`. Respects `site.indexable` setting.

### Configuration

```ts
// nuxt.config.ts
export default defineNuxtConfig({
  robots: {
    // Block AI crawlers
    blockAiBots: true,
    // Block non-SEO bots (reduces server load)
    blockNonSeoBots: true,
    // Custom rules
    groups: [
      { userAgent: '*', disallow: ['/admin'] }
    ]
  }
})
```

### Per-Page Control

```ts
// Disable indexing
useRobotsRule('noindex, nofollow')

// Object syntax with AI directives
useRobotsRule({
  noindex: true,
  nofollow: true,
  noai: true,           // Block AI training
  noimageai: true,      // Block AI image training
  'max-snippet': 150,   // Preview controls
  'max-image-preview': 'large'
})
```

Route rules:

```ts
export default defineNuxtConfig({
  routeRules: {
    '/admin/**': { robots: 'noindex, nofollow' },
    '/hidden': { robots: false }
  }
})
```

### Nuxt Content Frontmatter

```yaml
---
robots: noindex, nofollow
# Or structured:
robots:
  noindex: true
  nofollow: true
---
```

## Sitemap.xml

Auto-generated at `/sitemap.xml` from app routes.

### Configuration

```ts
// nuxt.config.ts
export default defineNuxtConfig({
  sitemap: {
    sources: ['/api/__sitemap__/urls'],
    exclude: ['/admin/**', '/secret'],
    // For static sites - no runtime generation
    zeroRuntime: true
  }
})
```

### Dynamic URLs via API

```ts
// server/api/__sitemap__/urls.ts
import { defineSitemapEventHandler } from '#imports'
import type { SitemapUrlInput } from '#sitemap/types'

export default defineSitemapEventHandler(async () => {
  const posts = await $fetch('/api/posts')
  return posts.map(post => ({
    loc: post.path,
    lastmod: post.updatedAt,
    // Image sitemap
    images: [{ loc: post.image, title: post.title }],
    // Video sitemap
    videos: [{ content_loc: post.videoUrl, title: post.title }]
  } satisfies SitemapUrlInput))
})
```

### Per-Page Control

Route rules:

```ts
export default defineNuxtConfig({
  routeRules: {
    '/blog/**': { sitemap: { changefreq: 'daily', priority: 0.9 } },
    '/hidden': { sitemap: false }
  }
})
```

Nuxt Content frontmatter:

```yaml
---
sitemap:
  changefreq: weekly
  priority: 0.8
  lastmod: 2025-01-15
---
```

### Multiple Sitemaps

For large sites:

```ts
export default defineNuxtConfig({
  sitemap: {
    sitemaps: {
      pages: { include: ['/**'], exclude: ['/blog/**'] },
      blog: { include: ['/blog/**'] }
    }
  }
})
```

Generates `/pages-sitemap.xml`, `/blog-sitemap.xml`, and `/sitemap_index.xml`.

### i18n Sitemaps

With `@nuxtjs/i18n`, auto-generates per-locale sitemaps with `hreflang` alternates.

## Debug

In development:

- Robots: Check `/robots.txt` directly
- Sitemap: Visit `/__sitemap__/debug.json` for raw data

---

## Supporting file: references/og-image.md

# OG Image Generation

Dynamic Open Graph image generation using Vue components.

## Quick Start

```ts
// Component-first (recommended)
defineOgImage('NuxtSeo', { title: 'My Page Title' })

// Object syntax
defineOgImage({ component: 'NuxtSeo', title: 'My Page Title' })

// Disable OG image
defineOgImage(false)
```

## Built-in Template

The `NuxtSeo` template supports:

```ts
defineOgImage('NuxtSeo', {
  title: 'Hello World',
  description: 'My description',
  theme: '#3b82f6',
  colorMode: 'dark',
  icon: 'carbon:cloud',
  siteName: 'My Site',
  siteLogo: '/logo.png'
})
```

## Multiple Images Per Page

Use `key` for platform-specific images:

```ts
// Default OG image (1200x600)
defineOgImage('NuxtSeo', { title: 'Default' })

// Square for WhatsApp (800x800)
defineOgImage('NuxtSeo', {
  title: 'Square',
  key: 'square',
  width: 800,
  height: 800
})
```

## Custom Vue Components

Create in `components/OgImage/`:

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

<template>
  <div class="w-full h-full flex flex-col justify-center items-center bg-gradient-to-br from-blue-500 to-purple-600 p-12">
    <h1 class="text-6xl font-bold text-white text-center">{{ title }}</h1>
    <p class="text-2xl text-white/80 mt-4">By {{ author }}</p>
  </div>
</template>
```

Use in pages:

```ts
defineOgImage('OgImageBlog', { title: 'My Post', author: 'John' })
```

## Renderers

| Renderer | Speed | CSS Support | Edge | Best For                   |
| -------- | ----- | ----------- | ---- | -------------------------- |
| satori   | Fast  | Partial     | ✅   | Default, most templates    |
| chromium | Slow  | Full        | ❌   | Complex designs, prerender |

```ts
export default defineNuxtConfig({
  ogImage: {
    defaults: { renderer: 'satori' }
  }
})
```

### Satori Limitations

- No `display: grid` - use `flex`
- No `position: absolute` without explicit dimensions
- Fonts: use `@nuxt/fonts` with `global: true` for best results

## Configuration

```ts
export default defineNuxtConfig({
  ogImage: {
    defaults: {
      component: 'NuxtSeo',
      width: 1200,
      height: 600,
      cacheMaxAgeSeconds: 60 * 60 * 24 * 3  // 3 days
    },
    // For static sites
    zeroRuntime: true
  }
})
```

## Nuxt Content

Frontmatter:

```yaml
---
ogImage:
  component: OgImageBlog
  props:
    author: John Doe
---
```

With `asSeoCollection()` (see main SKILL.md):

```vue
<script setup>
const { data: page } = await useAsyncData(() => queryCollection('posts').path(route.path).first())
if (page.value?.ogImage)
  defineOgImage(page.value.ogImage)
</script>
```

## Debug

- Preview: `/__og-image__/image/[path]/og.png`
- Inspector: Enable `ogImage: { debug: true }` in config

## Screenshots

Capture page as OG image (requires Chromium):

```ts
defineOgImageScreenshot({
  colorScheme: 'dark',
  mask: '.navigation, .footer',
  selector: '.article-content'
})
```

## Route Rules

```ts
export default defineNuxtConfig({
  routeRules: {
    '/blog/**': { ogImage: { component: 'OgImageBlog' } },
    '/admin/**': { ogImage: false }
  }
})
```

## Deployment

Community templates are dev-only. Before deploying, eject:

```bash
npx nuxt-og-image eject NuxtSeo
```

---

## Supporting file: references/schema-org.md

# Schema.org Structured Data

JSON-LD structured data for rich search results.

## Site Identity

Configure once in `nuxt.config.ts`:

```ts
import { defineOrganization } from 'nuxt-schema-org/schema'

export default defineNuxtConfig({
  schemaOrg: {
    identity: defineOrganization({
      name: 'My Company',
      url: 'https://example.com',
      logo: '/logo.png',
      sameAs: ['https://twitter.com/mycompany', 'https://github.com/mycompany']
    })
  }
})
```

For personal sites:

```ts
import { definePerson } from 'nuxt-schema-org/schema'

export default defineNuxtConfig({
  schemaOrg: {
    identity: definePerson({
      name: 'John Doe',
      url: 'https://johndoe.com',
      image: '/avatar.jpg',
      sameAs: ['https://twitter.com/johndoe']
    })
  }
})
```

## Page-Level Schema

Define functions are **auto-imported** in components (no import needed):

```ts
// Article page
useSchemaOrg([
  defineArticle({
    headline: 'My Article Title',
    description: 'Article description',
    image: '/article-image.jpg',
    datePublished: '2025-01-15',
    dateModified: '2025-01-20',
    author: { name: 'John Doe', url: 'https://johndoe.com' }
  })
])
```

```ts
// Product page (include url in offers for Google validation)
useSchemaOrg([
  defineProduct({
    name: 'Product Name',
    description: 'Product description',
    image: '/product.jpg',
    offers: {
      price: 99.99,
      priceCurrency: 'USD',
      availability: 'InStock',
      url: 'https://example.com/product'
    }
  })
])
```

## Define Functions

| Function                | Use Case               |
| ----------------------- | ---------------------- |
| `defineArticle()`       | Blog posts, news       |
| `defineProduct()`       | E-commerce products    |
| `defineFAQPage()`       | FAQ pages              |
| `defineHowTo()`         | Tutorial/guide pages   |
| `defineRecipe()`        | Recipe pages           |
| `defineEvent()`         | Events                 |
| `defineLocalBusiness()` | Business info          |
| `defineVideo()`         | Video content          |
| `defineBreadcrumb()`    | Breadcrumb navigation  |
| `defineWebPage()`       | Generic page           |
| `defineWebSite()`       | Site-wide (auto-added) |
| `defineJobPosting()`    | Job listings           |
| `defineSoftwareApp()`   | Software/apps          |
| `defineService()`       | Services               |

## Data Inference

Module auto-infers from page head:

- `title` → WebPage name
- `description` → WebPage description
- `og:image` → WebPage image

## Breadcrumbs

Auto-generated from route path, or customize:

```ts
useSchemaOrg([
  defineBreadcrumb({
    itemListElement: [
      { name: 'Home', item: '/' },
      { name: 'Blog', item: '/blog' },
      { name: 'My Post', item: '/blog/my-post' }
    ]
  })
])
```

Or use the `useBreadcrumbItems()` composable (from seo-utils):

```ts
const items = useBreadcrumbItems()
useSchemaOrg([defineBreadcrumb({ itemListElement: items })])
```

## FAQ Page

```ts
useSchemaOrg([
  defineFAQPage({
    mainEntity: [
      { name: 'What is your return policy?', acceptedAnswer: 'You can return within 30 days.' },
      { name: 'How do I contact support?', acceptedAnswer: 'Email us at support@example.com' }
    ]
  })
])
```

## Nuxt Content

Frontmatter:

```yaml
---
title: My Article
schemaOrg:
  - type: BlogPosting
    headline: My Article
    datePublished: 2025-01-15
    author:
      type: Person
      name: John Doe
---
```

With `asSeoCollection()` (see main SKILL.md), ensure schema renders:

```vue
<script setup>
const { data: page } = await useAsyncData(() => queryCollection('posts').path(route.path).first())
useHead(page.value?.head || {})
</script>
```

## Debug & Validation

- Debug endpoint: `/__schema-org__/debug.json` in dev
- Config: `schemaOrg: { debug: true }`
- [Google Rich Results Test](https://search.google.com/test/rich-results)
- [Schema.org Validator](https://validator.schema.org/)

## Route Rules

```ts
export default defineNuxtConfig({
  routeRules: {
    '/blog/**': {
      schemaOrg: { type: 'Article' }
    }
  }
})
```

---

## Supporting file: references/site-config.md

# Site Config

Foundation module providing shared configuration for all SEO modules.

## Configuration

```ts
// nuxt.config.ts
export default defineNuxtConfig({
  site: {
    url: 'https://example.com',       // Required for absolute URLs
    name: 'My Site',                  // Site name (used in titles, schema)
    description: 'Site description',  // Default meta description
    defaultLocale: 'en',              // Default language
    indexable: true,                  // Allow search engine indexing
    trailingSlash: false,             // URL trailing slash preference
  }
})
```

## Environment-Based Indexing

Control indexing per environment using `NUXT_SITE_*` env vars:

```bash
# .env.production
NUXT_SITE_URL=https://example.com
NUXT_SITE_ENV=production

# .env.staging
NUXT_SITE_URL=https://staging.example.com
NUXT_SITE_ENV=staging
```

The module auto-detects `env` and sets `indexable: false` for non-production environments.

For explicit control:

```ts
export default defineNuxtConfig({
  site: {
    url: process.env.NUXT_SITE_URL,
    // Explicit: only index when explicitly set to 'true'
    indexable: process.env.NUXT_SITE_INDEXABLE === 'true'
  }
})
```

**Note:** `!== 'false'` defaults to `true` when env var is undefined - use `=== 'true'` for fail-safe behavior.

## Runtime Access

```ts
const site = useSiteConfig()
console.log(site.url, site.name, site.description)
```

Works in components, composables, and server routes.

## i18n Integration

Automatically integrates with `@nuxtjs/i18n`:

```ts
export default defineNuxtConfig({
  site: {
    url: 'https://example.com',
    defaultLocale: 'en',
  },
  i18n: {
    locales: [
      { code: 'en', language: 'en-US' },
      { code: 'fr', language: 'fr-FR' },
    ]
  }
})
```

Locale-specific overrides in `site` object:

```ts
site: {
  name: 'My Site',
  locales: {
    fr: { name: 'Mon Site' }
  }
}
```

## Override Per-Page

Use route rules for page-specific config:

```ts
export default defineNuxtConfig({
  routeRules: {
    '/admin/**': { site: { indexable: false } },
    '/fr/**': { site: { name: 'Mon Site', defaultLocale: 'fr' } }
  }
})
```

---

## Supporting file: references/utilities.md

# SEO Utilities

Additional utilities from nuxt-seo-utils and nuxt-link-checker.

## Canonical URLs

Automatic canonical URLs based on site config.

```ts
export default defineNuxtConfig({
  seoUtils: {
    canonicalQueryWhitelist: ['page', 'sort'],  // Keep these query params
    redirectToCanonicalSiteUrl: true  // 301 to canonical domain
  }
})
```

Override per-page:

```ts
useHead({
  link: [{ rel: 'canonical', href: 'https://example.com/preferred-url' }]
})
```

## Breadcrumbs

Generate breadcrumb items from current route:

```ts
const items = useBreadcrumbItems()
// [{ label: 'Home', to: '/' }, { label: 'Blog', to: '/blog' }, { label: 'My Post' }]
```

For schema.org integration, see [schema-org.md](schema-org.md#breadcrumbs).

Render in template:

```vue
<template>
  <nav aria-label="Breadcrumb">
    <ol class="flex gap-2">
      <li v-for="(item, i) in items" :key="i">
        <NuxtLink v-if="item.to" :to="item.to">{{ item.label }}</NuxtLink>
        <span v-else>{{ item.label }}</span>
      </li>
    </ol>
  </nav>
</template>
```

Customize labels in route meta:

```ts
// pages/blog/[slug].vue
definePageMeta({
  breadcrumb: { label: 'Article' }
})
```

## Title Templates

Set site-wide title template:

```ts
// nuxt.config.ts
export default defineNuxtConfig({
  app: {
    head: {
      titleTemplate: '%s | My Site'
    }
  }
})
```

Override per-page:

```ts
useHead({
  title: 'Page Title',
  titleTemplate: '%s - Different Template'
})
```

## Meta Defaults

```ts
// nuxt.config.ts
export default defineNuxtConfig({
  app: {
    head: {
      meta: [
        { name: 'author', content: 'My Name' },
        { property: 'og:site_name', content: 'My Site' }
      ]
    }
  }
})
```

## Link Checker

Build-time validation of links.

```ts
export default defineNuxtConfig({
  linkChecker: {
    failOnError: true,  // Default: fail build on errors
    exclude: ['/api/**'],
    skipInspections: ['missing-hash'],
    report: { html: true }  // Generate HTML report
  }
})
```

**Inspections:**

- `no-error-response` - 404/500 errors
- `no-baseless` - Missing base URL
- `no-javascript` - javascript: links
- `trailing-slash` - Inconsistent slashes
- `missing-hash` - Invalid anchor targets
- `no-uppercase-chars` - URL casing
- `absolute-site-urls` - Hardcoded domain

### Ignoring Links

```html
<a href="/maybe-broken" data-link-checker-ignore>Link</a>
```

## File-Based Icons

Place favicon files in `public/`:

```
public/
├── favicon.ico
├── favicon.svg        # Modern browsers
├── apple-touch-icon.png
└── site.webmanifest
```

Auto-detected and added to `<head>`.

For SVG favicon with dark mode support:

```svg
<!-- public/favicon.svg -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
  <style>
    path { fill: #000; }
    @media (prefers-color-scheme: dark) {
      path { fill: #fff; }
    }
  </style>
  <path d="..."/>
</svg>
```

## Social Meta Tags

Automatic Open Graph and Twitter cards from site config (see [site-config.md](site-config.md)).

Override per-page:

```ts
useSeoMeta({
  title: 'Page Title',
  description: 'Page description',
  ogImage: '/images/page-og.png',
  twitterCard: 'summary_large_image'
})
```

## Trailing Slash Redirect

Enforce consistent URLs:

```ts
export default defineNuxtConfig({
  site: {
    trailingSlash: false  // Redirect /blog/ to /blog
  }
})
```

## Debug Panel

Enable comprehensive debug panel:

```ts
export default defineNuxtConfig({
  seo: { debug: true }
})
```

Shows in dev:

- Current meta tags
- Schema.org data
- OG image preview
- Sitemap/robots status

---

## Supporting file: skills/nuxt-content/SKILL.md

# Nuxt Content v3

Progressive guidance for content-driven Nuxt apps with typed collections and SQL-backed queries.

## When to Use

Working with:

- Content collections (`content.config.ts`, `defineCollection`)
- Remote sources (GitHub repos, external APIs via `defineCollectionSource`)
- Content queries (`queryCollection`, navigation, search)
- MDC rendering (`<ContentRenderer>`, prose components)
- Database configuration (SQLite, PostgreSQL, D1, LibSQL)
- Content hooks (`content:file:beforeParse`, `content:file:afterParse`)
- i18n multi-language content
- NuxtStudio or preview mode
- LLMs integration (`nuxt-llms`)

**For writing documentation:** use `document-writer` skill
**For Nuxt basics:** use `nuxt` skill
**For NuxtHub deployment:** use `nuxthub` skill (NuxtHub v1 compatible)

## Available Guidance

Read specific files based on current work:

- **[references/collections.md](references/collections.md)** - defineCollection, schemas, sources, content.config.ts
- **[references/querying.md](references/querying.md)** - queryCollection, navigation, search, surroundings
- **[references/rendering.md](references/rendering.md)** - ContentRenderer, MDC syntax, prose components, Shiki
- **[references/config.md](references/config.md)** - Database setup, markdown plugins, renderer options
- **[references/studio.md](references/studio.md)** - NuxtStudio integration, preview mode, live editing

## Loading Files

**Consider loading these reference files based on your task:**

- [ ] [references/collections.md](references/collections.md) - if setting up collections, schemas, or content.config.ts
- [ ] [references/querying.md](references/querying.md) - if using queryCollection, navigation, or search
- [ ] [references/rendering.md](references/rendering.md) - if rendering markdown/MDC or working with ContentRenderer
- [ ] [references/config.md](references/config.md) - if configuring database, markdown plugins, or renderer options
- [ ] [references/studio.md](references/studio.md) - if integrating NuxtStudio or preview mode

**DO NOT load all files at once.** Load only what's relevant to your current task.

## Key Concepts

| Concept         | Purpose                                                           |
| --------------- | ----------------------------------------------------------------- |
| Collections     | Typed content groups with schemas                                 |
| Page vs Data    | `page` = routes + body, `data` = structured data only             |
| Remote sources  | `source.repository` for GitHub, `defineCollectionSource` for APIs |
| queryCollection | SQL-like fluent API for content                                   |
| MDC             | Vue components inside markdown                                    |
| ContentRenderer | Renders parsed markdown body                                      |

## Quick Start

```ts
// content.config.ts
import { defineCollection, defineContentConfig, z } from '@nuxt/content'

export default defineContentConfig({
  collections: {
    blog: defineCollection({
      type: 'page',
      source: 'blog/**',
      schema: z.object({
        title: z.string(),
        date: z.date(),
      }),
    }),
  },
})
```

```vue
<!-- pages/blog/[...slug].vue -->
<script setup lang="ts">
const { data: page } = await useAsyncData(
  () => queryCollection('blog').path(useRoute().path).first()
)
</script>

<template>
  <ContentRenderer v-if="page" :value="page" />
</template>
```

**Verify setup:** Run `npx nuxi typecheck` to confirm collection types resolve. If `queryCollection` returns empty, check that content files exist in the path matching your `source` glob.

## Directory Structure

```
project/
├── content/                    # Content files
│   ├── blog/                   # Maps to 'blog' collection
│   └── .navigation.yml         # Navigation metadata
├── components/content/         # MDC components
└── content.config.ts           # Collection definitions
```

## Official Documentation

- Nuxt Content: https://content.nuxt.com
- MDC syntax: https://content.nuxt.com/docs/files/markdown#mdc-syntax
- Collections: https://content.nuxt.com/docs/collections/collections

## Token Efficiency

Main skill: ~300 tokens. Each sub-file: ~800-1200 tokens. Only load files relevant to current task.
