A comprehensive Sanity Studio v3 plugin to manage SEO fields like meta titles, descriptions, Open Graph tags, and X (Formerly Twitter) Cards for structured, search-optimized content.
- π― Meta Tags: Title, description, keywords, and canonical URLs
- π± Open Graph: Complete social media sharing optimization
- π¦ X (Formerly Twitter) Cards: X-specific meta tags with image support
- π€ Robots Control: Index/follow settings for search engines
- πΌοΈ Image Management: Optimized image handling for social sharing
- π Live Preview: Real-time SEO preview as you edit
- π§ TypeScript Support: Full type definitions included
- π Custom Attributes: Flexible meta attribute system
- β Validation: Built-in character limits and best practices
- ποΈ Field Visibility: Hide sitewide fields on specific content types
npm install sanity-plugin-seofieldsor
yarn add sanity-plugin-seofieldsAdd the plugin to your sanity.config.ts (or .js) file:
import {defineConfig} from 'sanity'
import seofields from 'sanity-plugin-seofields'
export default defineConfig({
name: 'your-project',
title: 'Your Project',
projectId: 'your-project-id',
dataset: 'production',
plugins: [
seofields(), // Add the SEO fields plugin
// ... other plugins
],
schema: {
types: [
// ... your schema types
],
},
})Add the SEO fields to any document type in your schema:
import {defineField, defineType} from 'sanity'
export default defineType({
name: 'page',
title: 'Page',
type: 'document',
fields: [
defineField({
name: 'title',
title: 'Title',
type: 'string',
}),
defineField({
name: 'content',
title: 'Content',
type: 'text',
}),
// Add SEO fields
defineField({
name: 'seo',
title: 'SEO',
type: 'seoFields',
}),
],
})You can also use individual components:
import {defineField, defineType} from 'sanity'
export default defineType({
name: 'article',
title: 'Article',
type: 'document',
fields: [
// ... other fields
// Individual SEO components
defineField({
name: 'openGraph',
title: 'Open Graph',
type: 'openGraph',
}),
defineField({
name: 'twitterCard',
title: 'X (Formerly Twitter) Card',
type: 'twitter',
}),
defineField({
name: 'metaAttributes',
title: 'Custom Meta Tags',
type: 'metaTag',
}),
],
})| Type | Description | Use Case |
|---|---|---|
seoFields |
Complete SEO package | Main SEO fields for any document |
openGraph |
Open Graph meta tags | Social media sharing |
twitter |
X (Formerly Twitter) Card settings | X-specific optimization |
metaTag |
Custom meta attributes | Advanced meta tag management |
metaAttribute |
Individual meta attribute | Building custom meta tags |
robots |
Search engine directives | Control indexing and crawling |
import seofields from 'sanity-plugin-seofields'
export default defineConfig({
plugins: [
seofields(), // Use default configuration
],
})You can customize field titles and descriptions, control SEO preview functionality, and manage field visibility:
import seofields, {SeoFieldsPluginConfig} from 'sanity-plugin-seofields'
export default defineConfig({
plugins: [
seofields({
seoPreview: true, // Enable/disable SEO preview (default: true)
fieldOverrides: {
title: {
title: 'Page Title',
description: 'The main title that appears in search results',
},
description: {
title: 'Meta Description',
description: 'A brief description of the page content for search engines',
},
canonicalUrl: {
title: 'Canonical URL',
description: 'The preferred URL for this page to avoid duplicate content issues',
},
metaImage: {
title: 'Social Media Image',
description: 'Image used when sharing this page on social media',
},
keywords: {
title: 'SEO Keywords',
description: 'Keywords that describe the content of this page',
},
},
// Hide sitewide fields on specific content types
fieldVisibility: {
page: {
hiddenFields: ['openGraphSiteName', 'twitterSite'],
},
post: {
hiddenFields: ['openGraphSiteName', 'twitterSite'],
},
},
// Or hide fields globally
defaultHiddenFields: ['openGraphSiteName', 'twitterSite'],
} satisfies SeoFieldsPluginConfig),
],
})| Option | Type | Default | Description |
|---|---|---|---|
seoPreview |
boolean |
true |
Enable/disable the live SEO preview feature |
fieldOverrides |
object |
{} |
Customize field titles and descriptions |
fieldVisibility |
object |
{} |
Hide sitewide fields on specific post types |
defaultHiddenFields |
array |
[] |
Hide sitewide fields globally |
Each field in the fieldOverrides object can have:
title- Custom title for the fielddescription- Custom description/help text for the field
Available field keys:
title,description,canonicalUrl,metaImage,keywords,metaAttributes,robotsopenGraphUrl,openGraphTitle,openGraphDescription,openGraphSiteName,openGraphType,openGraphImagetwitterCard,twitterSite,twitterCreator,twitterTitle,twitterDescription,twitterImage
Control which fields are visible on different content types. You can hide any SEO field on any post type:
Available field keys:
title,description,canonicalUrl,metaImage,keywords,metaAttributes,robotsopenGraphUrl,openGraphTitle,openGraphDescription,openGraphSiteName,openGraphType,openGraphImagetwitterCard,twitterSite,twitterCreator,twitterTitle,twitterDescription,twitterImage
βΉοΈ Hiding
openGraphImageortwitterImagealso hides their URL and type variants to keep the editor experience consistent.
Example configurations:
// Hide fields globally
seofields({
defaultHiddenFields: ['openGraphSiteName', 'twitterSite', 'keywords'],
})
// Hide fields on specific content types
seofields({
fieldVisibility: {
page: {
hiddenFields: ['openGraphSiteName', 'twitterSite', 'keywords'],
},
post: {
hiddenFields: ['openGraphSiteName', 'metaAttributes'],
},
product: {
hiddenFields: ['canonicalUrl', 'robots'],
},
},
})This is particularly useful when you want to:
- Manage sitewide settings (like site name and X handle) in a dedicated Site Settings document
- Simplify the editing experience by hiding fields that aren't relevant for certain content types
- Create different SEO workflows for different content types
- Max Length: 70 characters (warning at 60)
- Purpose: Search engine result headlines
- Best Practice: Include primary keywords, keep under 60 chars
- Max Length: 160 characters (warning at 150)
- Purpose: Search result descriptions
- Best Practice: Compelling summary with keywords
- Format: Must include protocol (https://)
- Purpose: Signals the preferred URL when duplicate or paginated content exists
- Best Practice: Mirror the resolved frontend route exactly to avoid mismatched indexing
- Recommended Size: 1200x630px minimum 600x315px
- Purpose: Default share image when Open Graph/Twitter images are absent
- Best Practice: Provide descriptive alt text and keep file size under 5MB
- Structure: Key/value pairs or key/image pairs
- Purpose: Add bespoke
<meta>tags (for exampletheme-color,author, verification tokens) - Best Practice: Avoid duplicating tags already generated elsewhere to limit head bloat
- Type: Array of short strings
- Purpose: Editorial helper; not surfaced automatically to search engines
- Best Practice: Keep entries concise (1-3 words) and limit to high-intent topics
- Recommended Size: 1200x630px
- Minimum Size: 600x315px
- Aspect Ratio: 1.91:1
- Formats: JPG, PNG, WebP
- Purpose: Canonical URL for social media sharing
- Format: Full URL with protocol (https://)
- Best Practice: Use the preferred URL for the page to avoid duplicate content issues
- Required: Should match the actual page URL for consistency
- Purpose: Displays publisher name on share previews
- Best Practice: Keep consistent with brand name used across marketing channels
- Options:
website,article,profile,book,music,video,product - Best Practice: Pick the narrowest type applicable to unlock platform-specific rendering
- Summary Card: Minimum 120x120px
- Large Image: Minimum 280x150px
- Required: Alt text for accessibility
- Purpose: Attribution to content creator on X (formerly Twitter)
- Format: X handle with @ symbol (e.g., @creator)
- Usage: Identifies the individual author of the content
- Best Practice: Use actual X handles for proper attribution
- Options:
summary,summary_large_image,app,player - Best Practice: Use
summary_large_imagefor rich media, fall back tosummarywhen imagery is square or minimal
- Purpose: Publisher attribution when multiple authors contribute
- Format: X handle with @ symbol (e.g., @brand)
- Best Practice: Configure once in site settings and hide on document types that inherit it
- Options:
noIndex,noFollow - Purpose: Control whether pages are indexed or links followed by crawlers
- Best Practice: Only enable when intentionally blocking content (for example gated pages or previews)
The field visibility feature allows you to hide any SEO field on specific content types. This is perfect for managing sitewide settings in a dedicated Site Settings document or creating customized editing experiences for different content types.
// Hide specific fields on different content types
seofields({
fieldVisibility: {
page: {
hiddenFields: ['openGraphSiteName', 'twitterSite', 'keywords'],
},
post: {
hiddenFields: ['openGraphSiteName', 'metaAttributes'],
},
product: {
hiddenFields: ['canonicalUrl', 'robots'],
},
},
})Create a Site Settings document to manage sitewide fields:
// schemas/siteSettings.ts
export default defineType({
name: 'siteSettings',
title: 'Site Settings',
type: 'document',
fields: [
defineField({
name: 'openGraphSiteName',
title: 'Open Graph Site Name',
type: 'string',
}),
defineField({
name: 'twitterSite',
title: 'X (Formerly Twitter) Site Handle',
type: 'string',
}),
],
})// In your schema
defineField({
name: 'seo',
title: 'SEO & Social Media',
type: 'seoFields',
group: 'seo', // Optional: group in a tab
})// For advanced users who need custom meta tags
defineField({
name: 'customMeta',
title: 'Custom Meta Tags',
type: 'metaTag',
description: 'Add custom meta attributes for specific needs',
})// If you only need Open Graph
defineField({
name: 'socialSharing',
title: 'Social Media Sharing',
type: 'openGraph',
})import Head from 'next/head'
export function SEOHead({seo}) {
if (!seo) return null
return (
<Head>
{seo.title && <title>{seo.title}</title>}
{seo.description && <meta name="description" content={seo.description} />}
{/* Open Graph */}
{seo.openGraph?.title && <meta property="og:title" content={seo.openGraph.title} />}
{seo.openGraph?.description && (
<meta property="og:description" content={seo.openGraph.description} />
)}
{seo.openGraph?.url && <meta property="og:url" content={seo.openGraph.url} />}
{/* Twitter */}
{seo.twitter?.card && <meta name="twitter:card" content={seo.twitter.card} />}
{seo.twitter?.site && <meta name="twitter:site" content={seo.twitter.site} />}
{seo.twitter?.creator && <meta name="twitter:creator" content={seo.twitter.creator} />}
{/* Robots */}
{seo.robots?.noIndex && <meta name="robots" content="noindex" />}
{seo.robots?.noFollow && <meta name="robots" content="nofollow" />}
{/* Canonical URL */}
{seo.canonicalUrl && <link rel="canonical" href={seo.canonicalUrl} />}
</Head>
)
}import {Helmet} from 'react-helmet'
export function SEO({seo}) {
return (
<Helmet>
<title>{seo?.title}</title>
<meta name="description" content={seo?.description} />
{/* Keywords */}
{seo?.keywords && <meta name="keywords" content={seo.keywords.join(', ')} />}
{/* Open Graph */}
<meta property="og:title" content={seo?.openGraph?.title} />
<meta property="og:description" content={seo?.openGraph?.description} />
<meta property="og:url" content={seo?.openGraph?.url} />
<meta property="og:type" content={seo?.openGraph?.type || 'website'} />
{/* Twitter */}
{seo?.twitter?.card && <meta name="twitter:card" content={seo.twitter.card} />}
{seo?.twitter?.site && <meta name="twitter:site" content={seo.twitter.site} />}
{seo?.twitter?.creator && <meta name="twitter:creator" content={seo.twitter.creator} />}
</Helmet>
)
}import seofields from 'sanity-plugin-seofields'seoFields- Complete SEO fields objectopenGraph- Open Graph meta tagstwitter- Twitter Card settingsmetaTag- Custom meta tag collectionmetaAttribute- Individual meta attributerobots- Search engine robots settings
Contributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add some amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
MIT Β© Desai Hardik
- π§ Email: dhardik1430@gmail.com
- π Issues: GitHub Issues
- π Documentation: Types & Schema Docs
Made with β€οΈ for the Sanity community