Nuxt Content
Introduction
Most Nuxt SEO modules integrate with Nuxt Content by default.
- Nuxt Robots:
robots(docs) - Nuxt Sitemap:
sitemap(docs) - Nuxt OG Image:
ogImage(docs) - Nuxt Schema.org:
schemaOrg(docs) - Nuxt Link Checker: Uses content APIs to check links
@nuxtjs/seo or the modules individually. Each module provides its own defineXxxSchema() function that you compose in your collection schema. You only need to include schemas for the modules you use.For Nuxt Content v3 you need to add each module's schema to your collection using their defineXxxSchema() functions.
Nuxt SEO v6 no longer supports Nuxt Content v2. If your app uses Content v2, upgrade to Content v3 first.
Setup Nuxt Content v3
The schema functions need zod. Install it as a direct dependency; with pnpm, a copy nested in another package does not resolve from your content.config.ts.
npx nypm add zodAdd each module's schema field to your collection using its defineXxxSchema() function. If you installed @nuxtjs/seo, import them from @nuxtjs/seo/content.
import { defineCollection, defineContentConfig } from '@nuxt/content'
import { defineOgImageSchema, defineRobotsSchema, defineSchemaOrgSchema, defineSitemapSchema } from '@nuxtjs/seo/content'
import { z } from 'zod'
export default defineContentConfig({
collections: {
content: defineCollection({
type: 'page',
source: '**/*.md',
schema: z.object({
robots: defineRobotsSchema(),
sitemap: defineSitemapSchema(),
ogImage: defineOgImageSchema(),
schemaOrg: defineSchemaOrgSchema(),
}),
}),
},
})If you installed the modules individually, import each function from its own module instead. Do not do this with only @nuxtjs/seo installed: under pnpm, @nuxtjs/robots/content and the other module paths do not resolve from your app.
import { defineRobotsSchema } from '@nuxtjs/robots/content'
import { defineSitemapSchema } from '@nuxtjs/sitemap/content'
import { defineOgImageSchema } from 'nuxt-og-image/content'
import { defineSchemaOrgSchema } from 'nuxt-schema-org/content'You only need to include the schemas for the modules you use. For example, if you only need sitemap and robots support, include those two:
import { defineCollection, defineContentConfig } from '@nuxt/content'
import { defineRobotsSchema, defineSitemapSchema } from '@nuxtjs/seo/content'
import { z } from 'zod'
export default defineContentConfig({
collections: {
content: defineCollection({
type: 'page',
source: '**/*.md',
schema: z.object({
robots: defineRobotsSchema(),
sitemap: defineSitemapSchema(),
}),
}),
},
})asSeoCollection() wrapper. If you still use it, follow the v5 to v6 migration guide.To ensure the tags gets rendered you need to ensure you're using the SEO composable.
<script setup lang="ts">
const route = useRoute()
const { data: page } = await useAsyncData(`page-${route.path}`, () => {
return queryCollection('content').path(route.path).first()
})
if (page.value?.ogImage?.component) {
defineOgImage({ component: page.value.ogImage.component, ...page.value.ogImage.props }) // <-- Nuxt OG Image
}
// Ensure the schema.org is rendered
useSchemaOrg(page.value.schemaOrg || []) // <-- Nuxt Schema.org
useSeoMeta(page.value.seo || {}) // <-- useSeoMeta
</script>The order of @nuxtjs/seo and @nuxt/content in your modules array does not matter.
Setup comark-content
comark-content is a Markdown-only content module. Nuxt SEO supports it first-party.
It needs no schema opt in. Declare the collection and write the frontmatter keys you want.
import { defineCollection, defineContentConfig } from '@harlan-zw/comark-content'
export default defineContentConfig({
collections: {
content: defineCollection({
type: 'page',
source: '**/*.md',
}),
},
})---
title: About
robots: false
sitemap:
priority: 0.8
schemaOrg:
"@type": "AboutPage"
---
# AboutThree differences from Nuxt Content v3:
- Every page collection is in the sitemap by default. Set
sitemap: falseon the collection to opt out. - The module supports only Markdown. There are no YAML, JSON, or CSV sources.
- Nuxt Sitemap names its data source
@harlan-zw/comark-content:urls, not@nuxt/content@v3:urls.
Per-module detail lives in each module's own guide, linked in Introduction.
Usage
For the full options available for each module i.e. for the schemas specific to each of the different SEO modules, please see the individual module documentation.
E.g.: The schema of the OG Image module (as provided by defineOgImageSchema() - see above) defines 3 properties:
component⇒ corresponding to a real component name as implemented within thecomponents/OgImagefolder.props⇒ holding the values of properties defined in the aforementioned component.url⇒ providing the address of an already existing (og compatible) image file, which the module uses as-is instead of generating an image on the fly.
---
title: "Hello World Page"
description: "The purpose of this page is to say 'hello!' to the world."
ogImage:
component: HelloWorld
props:
title: "Hello World"
description: "This is a description"
sitemap:
lastmod: 2025-01-01
robots: index, nofollow
schemaOrg:
- "@type": "BlogPosting"
headline: "How to Use Our Product"
author:
type: "Person"
name: "Jane Smith"
datePublished: "2023-10-01"
---
# {{$doc.title}}
{{ $doc.description}}
Using the following (example) og-image template "Simple.takumi" and the YAML data defined above
<script setup lang="ts">
const props = defineProps({
title: { type: String, required: false, default: 'title' },
description: { type: String, required: false },
})
</script>
<template>
<div style="background-color: darkgoldenrod;display: flex; flex-direction: column; font-size: 2rem; padding: 3rem; color: white">
<h1 style="font-size: 4rem; font-weight: 800;">
{{ title }}
</h1>
<h2 v-if="description" style="display: block;">
{{ description }}
</h2>
</div>
</template>You may now choose how to generate the og-image for any given content page:
Using the frontmatter properties
titleanddescription, since the schema of every "page" collection defines those properties by default - see Standard Collection Fields for the page collection type)[...slug].vue<script lang="ts" setup> const route = useRoute() const { data: page } = await useAsyncData(`page-${route.path}`, () => { return queryCollection('content').path(route.path).first() }) const [ogImagePath] = defineOgImage('Simple.takumi', { title: () => page.value?.title, description: () => page.value?.description }) useSeoMeta({ title: page.value.title, description: page.value.description, ogImage: ogImagePath, // ... }) </script> //...or
Using the
ogImagefrontmatter property and its attributescomponentandprops[...slug].vue<script lang="ts" setup> const route = useRoute() const { data: page } = await useAsyncData(`page-${route.path}`, () => { return queryCollection('content').path(route.path).first() }) const [ogImagePath] = defineOgImage(page.value.ogImage.component, page.value.ogImage.props) useSeoMeta({ title: page.value.title, description: page.value.description, ogImage: ogImagePath, // ... }) </script> //...Note: this would require that all content pages provide the adequate frontmatter data or
any combining (failover) strategy like
[...slug].vue<script lang="ts" setup> // ... const component = page.value?.ogImage?.component || 'Simple.takumi' const props = page.value?.ogImage?.props || { title: page.value?.title, description: page.value?.description } const [ogImagePath] = defineOgImage(component, props) // ... </script>
Note: Consider differentiating the multiple/different usages and requirements of such description fields on purpose:
<head>
<meta name="description" content="...">
<meta property="og:description" content="...">
<meta name="twitter:description" content="...">
<meta property="og:image:alt" content="...">
<!-- ... -->
</head>