The SEO Feature Most Sanity Developers Skip Entirely
Run a technical audit on most Sanity-powered sites and you'll find the same gap: metadata is present, content is well-structured, but there's zero JSON-LD. No schema.org markup. No structured data.
This matters more than it used to. Google's AI-powered features (rich results, SGE, knowledge panels) rely heavily on structured data. So do Bing Copilot, Perplexity, and ChatGPT when they crawl and cite content. Without JSON-LD, your content is semantically invisible to machine readers.
The problem isn't that developers don't know this. It's that adding JSON-LD to a headless CMS has historically been painful. You write raw JSON templates, maintain them as schemas evolve, and keep them in sync with content changes.
sanity-plugin-seofields solves this with a different approach: structured data is part of your CMS schema, not a hardcoded JSON blob.
Plugin resources: ๐ Documentation ยท ๐ฆ npm ยท ๐ป GitHub
Why SEO Belongs in Your CMS Schema
Headless CMS setups like Sanity give you incredible flexibility, but they also push the responsibility for structured metadata entirely onto you. Unlike WordPress (where Yoast handles this), there's no magic layer auto-generating SEO fields.
This means:
- No meta title by default
- No Open Graph images unless you define them
- No JSON-LD unless you implement it yourself
- No Twitter Cards unless your Next.js template handles it
The right solution isn't to write SEO utilities scattered across your frontend - it's to define SEO as first-class structured content in Sanity itself, then consume it cleanly in your framework.
Step 1: Install sanity-plugin-seofields
npm install sanity-plugin-seofields
# or
pnpm add sanity-plugin-seofieldsFull installation instructions and configuration options are available in the official documentation and on npm.
This plugin adds a full SEO field group - seoTitle, seoDescription, ogImage, twitterCard, canonicalUrl, and JSON-LD schema support - directly into your Sanity Studio document types.
Step 2: Register the Plugin
In your sanity.config.ts:
import { defineConfig } from 'sanity'
import { seoFields } from 'sanity-plugin-seofields'
export default defineConfig({
name: 'default',
title: 'My Project',
projectId: 'your-project-id',
dataset: 'production',
plugins: [
seoFields(),
],
schema: {
types: schemaTypes,
},
})Step 3: Add SEO Fields to Your Document Schema
// schemas/page.ts
import { defineType, defineField } from 'sanity'
export const page = defineType({
name: 'page',
title: 'Page',
type: 'document',
fields: [
defineField({
name: 'title',
type: 'string',
}),
defineField({
name: 'slug',
type: 'slug',
options: { source: 'title' },
}),
defineField({
name: 'seo',
title: 'SEO',
type: 'seoFields', // Provided by the plugin
}),
],
})Now editors see a clean SEO panel inside every document - with character counters, preview hints, and image pickers.
Step 4: Query SEO Data via GROQ
*[_type == "page" && slug.current == $slug][0]{
title,
"seo": seo{
seoTitle,
seoDescription,
canonicalUrl,
ogImage{
asset->{ url }
},
twitterCard,
jsonLd
}
}Step 5: Wire It Into Next.js App Router
// app/[slug]/page.tsx
import { client } from '@/sanity/client'
import type { Metadata } from 'next'
export async function generateMetadata({ params }): Promise<Metadata> {
const page = await client.fetch(PAGE_QUERY, { slug: params.slug })
const { seo } = page
return {
title: seo?.seoTitle ?? page.title,
description: seo?.seoDescription,
alternates: {
canonical: seo?.canonicalUrl,
},
openGraph: {
title: seo?.seoTitle ?? page.title,
description: seo?.seoDescription,
images: seo?.ogImage?.asset?.url
? [{ url: seo.ogImage.asset.url }]
: [],
},
twitter: {
card: seo?.twitterCard ?? 'summary_large_image',
title: seo?.seoTitle ?? page.title,
description: seo?.seoDescription,
},
}
}That's it. Five steps. Your Sanity project now has:
- โ Per-document SEO titles and descriptions
- โ Open Graph metadata
- โ Twitter/X Card metadata
- โ Canonical URL control
- โ JSON-LD schema fields
What About Existing Documents?
If you're adding SEO to an existing project, the plugin handles graceful nulls โ no migrations needed. Fields render as empty in the studio; your frontend falls back to document titles. Roll it out document type by document type at your own pace.
Performance Note
SEO fields add negligible weight to your GROQ queries. Sanity's CDN caches query responses, so metadata rendering adds zero runtime overhead in Next.js with ISR or static generation.
FAQ
Q: Does this work with Sanity v3 and v5? Yes. sanity-plugin-seofields is compatible with Sanity Studio v3+ and fully updated for v5.
Q: Can I extend the SEO fields with custom logic? Absolutely. The plugin provides a base schema you can extend with defineField overrides or custom validation rules.
Q: What happens if an editor leaves SEO fields blank? Your Next.js generateMetadata function should fall back to document title and description - build defensive fallbacks.
Q: Does JSON-LD get injected automatically? The plugin stores JSON-LD data in Sanity. You render it in your layout using a <Script> tag with type="application/ld+json".
Conclusion
Adding SEO to Sanity doesn't have to be a multi-sprint project. With sanity-plugin-seofields, you get a structured, editor-friendly, developer-controlled SEO layer that plugs directly into Next.js App Router metadata. Ship it once, use it everywhere.
โ Get started now:
- ๐ Read the full documentation
- ๐ฆ Install from npm
- ๐ป Explore the source on GitHub
