@crawl-me-maybe/sanity-plugin-seo
v0.1.1
Published
<img width="1590" height="408" alt="sanity-plugin-thumb" src="https://github.com/user-attachments/assets/1069b4d7-7a3c-4ceb-b538-b0f3bcbe4c71" /> <br/>
Downloads
279
Readme
@crawl-me-maybe/sanity-plugin-seo
SEO fields, global defaults, and social preview cards for Sanity Studio.
Built for Sanity v5 and v6
Designed to pair with @crawl-me-maybe/meta on the frontend. The globalSeoSettings document and pageMetadata object use the same field names and shapes that buildMetadata() expects — query your content, resolve image URLs in GROQ, and pass the result straight through.
Table of contents
Install
npm install @crawl-me-maybe/sanity-plugin-seo
pnpm add @crawl-me-maybe/sanity-plugin-seo
bun add @crawl-me-maybe/sanity-plugin-seo
yarn add @crawl-me-maybe/sanity-plugin-seoFeatures
- Global defaults — a
globalSeoSettingssingleton document that feeds fallback values to all page-level fields; editors always see what fallback is being displayed - Page metadata — a
pageMetadataobject type with meta description, meta image, and search indexing controls that each show and override the global default - Social preview cards — live editable previews for Facebook, Twitter/X, LinkedIn, and Google Search, built from page data
- Favicon browser-tab preview — renders your favicon and site title in a mock browser tab with light/dark toggle
- Robots.txt rules editor — structured rule builder with a read-only
robots.txtpreview tab
Getting started
Plugin configuration
Pass options to the plugin to enable or disable individual features. All options default to true.
// sanity.config.ts
import crawlMeMaybeSeo from "@crawl-me-maybe/sanity-plugin-seo";
export default defineConfig({
plugins: [
crawlMeMaybeSeo({
global: {
favicon: true, // favicon field + browser-tab preview in Global Settings
robots: true, // robots.txt rule builder in Global Settings
},
page: {
searchIndexing: true, // noIndex / noFollow controls on page metadata
canonicalUrl: true, // canonical URL override on page metadata
},
}),
],
});| Option | Type | Default | Description |
|---|---|---|---|
| global.favicon | boolean | true | Includes the favicon field (with browser-tab preview) in Global SEO Settings |
| global.robots | boolean | true | Includes the robots rules builder in Global SEO Settings |
| page.searchIndexing | boolean | true | Includes noIndex / noFollow controls on the pageMetadata field |
| page.canonicalUrl | boolean | true | Includes the canonical URL field on pageMetadata. Disable if you manage canonical via a custom reference field on the document |
Custom canonical URLs
By default, pageMetadata includes a canonicalUrl field for explicit URL overrides. Disable it when you prefer a reference to another document:
// sanity.config.ts
crawlMeMaybeSeo({
page: {
canonicalUrl: false,
},
});Then add your own field on the page document:
defineField({
name: "canonical",
type: "reference",
to: [{ type: "page" }, { type: "post" }],
})Resolve the referenced document to a URL in your frontend query and pass it to @crawl-me-maybe/meta with disableSelfCanonical: true. When the built-in field is enabled, leave it empty and @crawl-me-maybe/meta generates a self-referential canonical from siteUrl + slug automatically.
Adding SEO fields to a page document
Include the pageMetadata object type on any document schema. Global defaults are displayed automatically in Studio; use @crawl-me-maybe/meta's buildMetadata() on the frontend to apply the same merge logic.
// schemas/page.ts
import { defineType, defineField } from "sanity";
export default defineType({
name: "page",
title: "Page",
type: "document",
fields: [
defineField({
name: "title",
title: "Title",
type: "string",
}),
defineField({
name: "slug",
title: "Slug",
type: "slug",
options: { source: "title" },
}),
defineField({
name: "seo",
title: "SEO",
type: "pageMetadata", // registered by the plugin
}),
],
});Each pageMetadata field shows the live global default as a placeholder when the field is empty, and renders social preview cards (Facebook, Twitter/X, LinkedIn, Google) in a tabbed panel.
Studio structure — surfacing Global SEO Settings
Register the plugin, then add the globalSeoSettings singleton to your Studio's structure so editors can reach it:
// sanity.config.ts
import { defineConfig } from "sanity";
import { structureTool } from "sanity/structure";
import crawlMeMaybeSeo from "@crawl-me-maybe/sanity-plugin-seo";
export default defineConfig({
plugins: [
structureTool({
structure: (S) =>
S.list()
.title("Content")
.items([
// ... your other list items ...
S.divider(),
S.listItem()
.title("Global SEO Settings")
.child(
S.document()
.schemaType("globalSeoSettings")
.documentId("globalSeoSettings")
),
]),
}),
crawlMeMaybeSeo(),
],
});Favicons
Browser-tab preview for dynamic favicons with theme-switching button. Can be disabled by setting global.favicon to false.
Robots.txt
Preview-enabled array field for robots.txt entries. Disabled by setting global.robots to false.
🧠 Note: This plugin only generates a preview, you are still responsible for exposing and formatting this to your frontend.
Frontend integration
This plugin only stores SEO metadata inside Sanity. Your app is responsible for reading these fields and rendering meta tags, canonical URLs, schema markup, robots.txt, and sitemap.xml.
@crawl-me-maybe/meta handles the metadata side — merging page and global defaults, title templates, Open Graph, Twitter cards, and canonical URLs. @crawl-me-maybe/sitemap and @crawl-me-maybe/schema cover sitemaps, robots.txt, and JSON-LD.
With @crawl-me-maybe/meta
The plugin schemas map directly to buildMetadata() inputs — no adapter layer required:
| Plugin schema | buildMetadata argument | Notes |
|---|---|---|
| globalSeoSettings | globalSeoDefaults (GlobalSeoSettings) | siteTitle, pageTitleTemplate, siteUrl, metaDescription, twitterHandle |
| globalSeoSettings.defaultMetaImage | defaultMetaImage | Resolve to a URL string in GROQ (defaultMetaImage.asset->url) |
| globalSeoSettings.favicon | faviconUrl | Resolve to a URL string in GROQ (favicon.asset->url) |
| Page title + slug | page.title, page.slug | Required for title templates and self-canonical URLs |
| pageMetadata.description | page.description | Falls back to globalSeoSettings.metaDescription |
| pageMetadata.metaImage | page.metaImage | Resolve to a URL string in GROQ; falls back to defaultMetaImage |
| pageMetadata.canonicalUrl | page.canonicalUrl | Path or full URL; empty values become a self-canonical from siteUrl + slug |
| pageMetadata.searchIndexing | page.searchIndexing | noIndex / noFollow become a robots meta tag |
Page query:
*[_type == "page" && slug.current == $slug][0]{
title,
"slug": { "current": slug.current },
"description": seo.description,
"canonicalUrl": seo.canonicalUrl,
"searchIndexing": seo.searchIndexing,
"metaImage": seo.metaImage.asset->url
}Global defaults query:
*[_type == "globalSeoSettings"][0]{
siteTitle,
pageTitleTemplate,
metaDescription,
siteUrl,
twitterHandle,
"defaultMetaImage": defaultMetaImage.asset->url,
"faviconUrl": favicon.asset->url
}Next.js example:
// app/[slug]/page.tsx
import type { Metadata } from "next";
import { buildMetadata } from "@crawl-me-maybe/meta";
import { toNextMeta } from "@crawl-me-maybe/meta/next";
import { client } from "@/lib/sanity";
export async function generateMetadata({ params }): Promise<Metadata> {
const page = await client.fetch(PAGE_QUERY, { slug: params.slug });
const seoDefaults = await client.fetch(GLOBAL_SEO_QUERY);
return toNextMeta(buildMetadata(page, seoDefaults));
}See the @crawl-me-maybe/meta docs for Nuxt, raw HTML, canonical URL options, and build-time overrides.
Schemas
globalSeoSettings document
A singleton document that provides site-wide defaults. Page-level fields display these values in Studio when empty; @crawl-me-maybe/meta's buildMetadata() applies the same fallbacks on the frontend.
| Field | Sanity type | Required | Validation | Notes |
|---|---|---|---|---|
| siteTitle | string | ✅ | Required | Injected into pageTitleTemplate via {siteTitle} |
| pageTitleTemplate | string | ✅ | Required | Custom token input; initial value {pageTitle} - {siteTitle} |
| siteUrl | url | ✅ | Must start with https:// | Used when generating canonical URLs, Open Graph URLs, and sitemap entries. |
| metaDescription | metaDescription | — | Warn < 120 or > 160 chars | Default description displayed by page metadata fields |
| defaultMetaImage | metaImage | — | — | Default OG image displayed by page metadata fields |
| favicon | image | — | — | Browser-tab preview in Studio. Controlled by global.favicon option |
| twitterHandle | string | — | Must start with @ | Social group |
| logo | image | — | — | Used for Organization / WebSite schema markup |
| advanced.robots | robots[] | — | Paths must start with / | robots.txt rule builder with preview tab. Controlled by global.robots option |
pageMetadata object
Add this type to any document that needs per-page SEO. Fields display the active global default as placeholder when empty.
| Field | Sanity type | Required | Validation | Notes |
|---|---|---|---|---|
| description | metaDescription | — | Warn < 120 or > 160 chars | Overrides globalSeoSettings.metaDescription |
| canonicalUrl | string | — | Path (/about) or https:// URL | Optional override; leave empty for auto self-canonical on the frontend. Controlled by page.canonicalUrl option |
| searchIndexing | searchIndexing | — | — | noIndex / noFollow controls. Controlled by page.searchIndexing option |
| metaImage | metaImage | — | — | Overrides globalSeoSettings.defaultMetaImage; shows thumbnail of active default when empty |
Shared field types
These named types are registered by the plugin and can be reused in your own schemas:
| Type name | Base type | Description |
|---|---|---|
| metaDescription | text | 3-row textarea with 120–160 char warnings |
| metaTitle | string | Single-line title field with 50–60 char warnings |
| metaImage | image | Standard image field for OG/social use |
| favicon | image | Image with browser-tab preview component |
| searchIndexing | object | noIndex + noFollow boolean controls |
| robots | array | Rule builder with userAgent / allow / disallow fields and robots.txt preview |
