Sanity SEO

Sanity stores content as JSON and emits zero HTML — your frontend decides your SEO. Rendering modes, Portable Text, the SEO object, sitemaps, draft-mode noindex, JSON-LD, and plugins.

First published: Jun 27, 2026 · Last updated: Jul 19, 2026 · Advanced
demand #3 in Headless CMS#36 in Platform SEO#224 in Technical SEO#304 on the site

Sanity is a headless CMS that stores content as structured JSON in its Content Lake and produces no HTML — so your frontend framework (Next.js, Astro, Remix) decides everything about your SEO. The single biggest lever is rendering: SSG and SSR ship complete HTML and are safe; CSR is risky because LLM-crawler rendering contracts vary by provider. Portable Text is a JSON AST, not HTML, so the frontend must serialize it. Build a reusable SEO object in the content model, generate sitemaps and JSON-LD programmatically, keep drafts out of the index with an X-Robots-Tag: noindex header, and use sanity-plugin-seo (not the deprecated Yoast pane). SEO success in Sanity comes from intentional structure, not plugins.

TL;DR — Sanity is content-neutral: it stores structured JSON in the Content Lake and emits zero HTML, so the frontend’s renderingTurning HTML, CSS, and JavaScript into the final visual page and DOM. mode decides everything. SSG and SSR ship complete HTML and are safe; CSR is the risky one because LLMA large language model (LLM) is a deep-learning model trained on massive text corpora to predict the next token and generate human-like text. LLMs use the transformer architecture and power AI search features like Google's AI Overviews (Gemini) and Bing Copilot (GPT-4). crawlersA crawler — also called a spider or bot — is an automated program that fetches web pages, extracts their links, and queues new URLs to visit. Search engines use crawlers to discover and download content for their index. don’t execute JavaScript, and Google deprecated dynamic renderingTurning HTML, CSS, and JavaScript into the final visual page and DOM. as a workaround. Portable Text is a JSON AST that the frontend must serialize. Build a reusable seo object in the content model with overrides (not requirements) via coalesce(), generate sitemapsA sitemap is a file that lists the pages, images, videos, and other files on your site so search engines can discover them. It helps discovery, but submitting a sitemap doesn't guarantee crawling or indexing. and JSON-LDJSON-LD (JavaScript Object Notation for Linked Data) is a script-based structured data format, typically paired with the schema.org vocabulary to describe page content for search engines and AI systems. Google recommends it over Microdata and RDFa because it's the easiest format to implement and maintain at scale — but all three work, and structured data isn't a ranking signal. programmatically from existing fields, and keep drafts out of the indexStoring a crawled page in the search index so it can appear in results. Crawled is not the same as indexed — Google selects what to keep, and indexing isn't guaranteed. with an X-Robots-Tag: noindex header on every preview response. Use sanity-plugin-seo — never the deprecated Yoast pane — but remember “SEO success in Sanity comes from intentional structure, not plugins.”

Sanity is content storage, not a publisher

A useful first principle: Sanity touches your content, never your rendering. The Content Lake stores everything as structured JSON, queried with GROQ (Sanity’s query language) or REST. Sanity Studio — the editing UI — is a React single-page app that usually lives on its own .sanity.studio subdomain and isn’t something you index. Nothing in this stack produces HTML.

Evidence for this claim Sanity Content Lake is queried using APIs and GROQ; it is distinct from a site's frontend rendering layer. Scope: Sanity-hosted content data and query APIs. Confidence: high · Verified: Sanity: Content Lake

So the entire SEO surface lives in your frontend framework. As Webstacks put it, “Sanity handles content storage and editing, while your frontend framework handles SEO output.” That’s not a limitation — it’s the whole headless model. Sanity’s own team frames it well: “When using Sanity as a CMSA content management system (CMS) is software that lets users create, manage, and publish digital content — like blog posts and pages — without writing raw code. WordPress, Drupal, and Joomla are the most common open-source CMS platforms., you’re doing headless SEO. Although this approach requires thoughtful technical implementation, it can liberate your content to help you reach more users across all channels.” The work is real, but it’s the same work as any headless build (see SEO for a Headless CMSA headless CMS decouples content storage and editing (the backend) from how that content is rendered and delivered (the frontend), serving content over an API instead of a built-in templated 'head'. Its SEO outcomes come almost entirely from how the separate frontend renders pages.).

Rendering mode is the foundation

Because the frontend builds the HTML, the rendering decision is the single most consequential SEO choice on a Sanity site. Four modes:

SSG — Static Site Generation. Content is fetched from Sanity at build time and baked into static HTML served from a CDN. Best-case SEO: fully-rendered HTML on first request, fast TTFBTime to First Byte — the time from the start of a request to when the first byte of the response arrives. It's a diagnostic metric (not a Core Web Vital) and a major input to FCP and LCP; ≤0.8 s is good.. Ideal for blog posts, marketing pages, and product/landing pages. The tradeoff is freshness — changed content needs a rebuild (ISR solves this).

SSR — Server-Side Rendering. Content is fetched per request and the full HTML is rendered on the server. Excellent SEO, always fresh. Use it for frequently updated content. Tradeoff is infrastructure cost and slightly higher TTFBTime to First Byte — the time from the start of a request to when the first byte of the response arrives. It's a diagnostic metric (not a Core Web Vital) and a major input to FCP and LCP; ≤0.8 s is good. than static files.

ISR — Incremental Static Regeneration (Vercel/Next.js). Static pages regenerate in the background, ideally triggered by a Sanity webhook on publish so the page rebuilds the moment an editor hits publish. This is the best of SSG’s speed and SSR’s freshness for content that changes on the order of hours or days.

CSR — Client-Side Rendering. An HTML shell ships, then JavaScript fetches from Sanity and builds the DOM in the browser. This is the worst SEO option: GooglebotGooglebot is Google's web crawler — the software that fetches pages so Google can index and rank them. It comes in two variants, Googlebot Smartphone (primary, under mobile-first indexing) and Googlebot Desktop, and runs an evergreen Chromium renderer. has to queue the page for a later render wave, timing is unpredictable, and AI crawlersA crawler — also called a spider or bot — is an automated program that fetches web pages, extracts their links, and queues new URLs to visit. Search engines use crawlers to discover and download content for their index. and many other bots see only the shell. CSR is acceptable only behind authentication for content you don’t want indexed anyway.

And don’t reach for the old shortcut: Google has deprecated dynamic rendering (serving prerendered HTML to bots, CSR to users). Officially, “dynamic rendering was a workaround and not a long-term solution,” and Google now recommends “server-side rendering, static rendering, or hydrationActivating server-rendered HTML in the browser by attaching JavaScript handlers..” For a Sanity site, that means SSG or SSR — not a bot-detection hack. (Full rendering treatment in JavaScript SEOMaking sure search engines can crawl, render, and index content that depends on JavaScript..)

Evidence for this claim Google describes dynamic rendering as a workaround rather than a recommended long-term solution and recommends server-side rendering, static rendering, or hydration. Scope: Google Search guidance for JavaScript sites. Confidence: high · Verified: Google: Dynamic rendering

Portable Text — your body copy is a JSON AST

This is the most Sanity-specific thing on the page. Sanity stores rich text as Portable Text: a JSON abstract syntax tree, not HTML. Body copy in the Content Lake looks like an array of typed blocks, not <p> tags. Your frontend has to serialize it into HTML before any crawler can read it:

  • @portabletext/react — for React/Next.js.
  • @portabletext/to-html — framework-agnostic (Astro, Remix, anything).
  • The old @sanity/block-content-to-react is deprecated — don’t use it.

If you render server-side or at build time, serialization happens before the page is sent and crawlers get full HTML. If you render client-side, the serialization runs in the browser. Because LLM-crawler rendering contracts vary by provider, CSR Portable Text creates a coverage risk that raw HTML avoids. There’s also a handy GROQ helper: pt::text(body) extracts all the plain text from a Portable Text field as a string, which is perfect for feeding JSON-LDJSON-LD (JavaScript Object Notation for Linked Data) is a script-based structured data format, typically paired with the schema.org vocabulary to describe page content for search engines and AI systems. Google recommends it over Microdata and RDFa because it's the easiest format to implement and maintain at scale — but all three work, and structured data isn't a ranking signal. descriptions without a separate editor field.

Content modeling for SEO — the reusable SEO object

The cleanest pattern, and the one Sanity’s own course teaches, is a single reusable seo object added to every document type — not SEO fields copy-pasted per type. The key design principle from Sanity Learn: “For the benefit of content authors, fields relevant to SEO should not always be required. Instead, they should be used to override some existing content, when provided.”

In practice that means SEO fields are overrides with fallbacks, implemented with GROQ’s coalesce():

"title": coalesce(seo.title, title, ""),
"description": coalesce(seo.description, excerpt, "")

A minimum viable seo object: metaTitle (≤65 chars), metaDescription (≤155 chars), canonicalUrl, openGraphImage (1200×630), and a noIndex boolean. Add Sanity validation rules to enforce those character limits before publish — so editors catch problems the platform would otherwise let through. Use a slug type with a custom generation function, and use references for internal linksAn internal link is a hyperlink from one page on a website to another page on the same website. Internal links help search engines discover your pages and pass ranking signals (PageRank and anchor-text context) between them. so URL changes propagate automatically. Add a hideFromSearch boolean and filter it out of both the sitemapA sitemap is a file that lists the pages, images, videos, and other files on your site so search engines can discover them. It helps discovery, but submitting a sitemap doesn't guarantee crawling or indexing. GROQ query and the page’s robots metadata.

Metadata, sitemaps, and canonicals — all built in the frontend

Metadata. In Next.js App Router, use the exported generateMetadata() function (not inline &lt;head&gt; tags) so metadata doesn’t duplicate across nested layouts. Build one server-side helper that takes a GROQ result and returns the framework’s metadata object — implement it once, use it everywhere. Open GraphOpen Graph (OG) tags are `<meta>` elements in a page's head, defined by the Open Graph protocol (ogp.me, created by Facebook), that describe a page as a shareable object — its title, description, image, URL, and type. They control how a link preview card looks when the page is shared on Facebook, LinkedIn, Slack, Discord, WhatsApp, and iMessage. They are not a direct Google ranking factor, though Google reads og:title, og:image, and og:site_name as inputs to how a result appears. images can be generated dynamically with Next.js Edge OG generation, pulling content straight from Sanity.

Sitemaps. There’s no auto-sitemap. You build it from a GROQ query → URL array → XML (sitemap.ts in Next.js, @astrojs/sitemap in Astro). Always filter drafts and hidden pages:

*[_type in ["page", "post"] && defined(slug.current) && hideFromSearch != true]

Use _updatedAt (not _createdAt or a manual date) for lastModified, keep individual sitemap files under 50,000 URLs with an index for larger sites, and trigger regeneration via a Sanity webhook on publish/unpublish so it never goes stale.

Canonicals. Set canonical URLs from the content model, default to a self-referencing canonical, and build absolute URLs from a single SITE_URL so the canonical never drifts. (Mechanics in CanonicalizationHow search engines pick one canonical URL among duplicates and consolidate signals onto it..)

Structured data — generate it, don’t author it

The right pattern is to generate JSON-LD programmatically from existing content fields at render time — never build a separate JSON-LD editor surface in Studio, which just creates drift between content and markup. Roboto Studio’s rule is blunt: “Derive JSON-LD from existing fields, at render time, in code.” Sanity agrees — “JSON-LD is a powerful way to provide structured dataStructured data is a standardized way of labeling page content (using the schema.org vocabulary in JSON-LD, Microdata, or RDFa) so search engines can understand its meaning. It's not a direct ranking factor — its value is rich results and entity understanding. to search engines — fortunately structured data is what Sanity does best.”

Use pt::text(body) to pull plain text for schema descriptions, the TypeScript package schema-dts for type safety, and render it with &lt;script type="application/ld+json"&gt;. Note that “the JSON-LD markup can be rendered anywhere in the page — it doesn’t need to be stored inside the &lt;head&gt;.” Priority types for content sites: Article/BlogPosting, BreadcrumbList, FAQPage, Organization, WebSite.

Keeping drafts out of Google — the real-incident pitfall

Sanity’s perspective system controls draft visibility: published returns only production content; drafts returns draft documents for preview. Always use the published perspective in production API calls. But that alone isn’t enough, because draft URLs leak. As one developer recounted, the danger is “a client panicking because an unpublished landing page showed up in Google Search Console. If a content editor shares that URL in Slack, and someone clicks it from a browser that’s then crawled (it happens), the draft content can end up indexed.”

The reliable defense is a multi-layer one:

  1. Use the published perspective in production; never expose an unprotected preview endpoint.
  2. Add an X-Robots-Tag: noindex HTTP header to every draft-mode/preview response — even if a draft URL leaks and gets crawled, GooglebotGooglebot is Google's web crawler — the software that fetches pages so Google can index and rank them. It comes in two variants, Googlebot Smartphone (primary, under mobile-first indexing) and Googlebot Desktop, and runs an evergreen Chromium renderer. respects the header and won’t index it. (Don’t rely on robots.txtA plain-text file at the root of a host that tells crawlers which URLs they may and may not request. It controls crawling, not indexing — a blocked URL can still be indexed if it's linked from elsewhere. for this; a header is more reliable than a disallow for preview pages.)
  3. Validate the preview secret server-side; never reflect it in the redirectA redirect sends browsers and crawlers from a requested URL to a different one. An HTTP redirect specifically is a 3xx status code paired with a Location header; meta refresh and JavaScript redirects achieve a similar navigation without being a 3xx response themselves. Permanent redirects (301/308) are Google's signal the target should be canonical; temporary ones (302/303/307) aren't. URL, and scope the preview-enable endpoint to your Studio’s origin.

Robots, redirects, image SEO, and hreflang

robots.txt is a static frontend file (app/robots.ts in Next.js). Block Studio paths only if they’re on the same domain (Disallow: /studio/); don’t try to block preview pages with it.

Redirects are a genuine workflow win: store them as Sanity documents (from, to, statusCode) so the content team can manage redirects without a developer deploy, then implement them in Next.js middleware.ts or a CDN/Worker layer (never in Sanity itself). Add validation rules to prevent loops and invalid paths.

Image SEOImage SEO is optimizing the images on your pages so search engines can discover, crawl, index, and rank them — in Google Images and visual search, and as part of standard web results. It spans file format, filenames, alt text, compression, responsive markup, structured data, and image sitemaps.: use Sanity’s image CDN with ?auto=format for automatic WebP/AVIF, manage alt textAlt text is the value of the `alt` attribute on an HTML `<img>` element — a short text substitute chosen for the image's purpose and context, not a literal description of what it shows. It makes images accessible to screen-reader users and helps search engines understand images, mainly for image search. at the asset level with document-level overrides, use vanity filenames instead of hashed URLs, and set hotspot/crop so automatic crops keep the subject — which matters most for OG images.

HreflangHreflang is an annotation (in HTML, HTTP headers, or XML sitemaps) that tells search engines which language and optional region a page targets, and which alternate versions exist. It only works when every page in the cluster references all the others.: model locale variants as separate Sanity documents referencing the canonical version, generate hreflang from a GROQ query returning all locale slugs, and give every locale page a self-referencing canonical — never canonical a non-English page to the English one.

Plugins — helpful, not a substitute

  • sanity-plugin-seo — the primary recommendation (22k+ downloads). Live SEO score, meta/OG preview, social cards, robots control, hreflang, readability. The AI tier adds keyword suggestions and meta generation.
  • sanity-plugin-seofields — an alternative: 39 schema.org types, an SEO Health Dashboard, and Next.js helper functions (buildSeoMeta()).
  • sanity-plugin-seo-paneDEPRECATED. This is the old Yoast integration, removed due to an outdated dependency. It still shows up in old tutorials. Don’t use it.

The honest framing, from Yanatiev: “SEO success in Sanity comes from intentional structure, not plugins.” Plugins surface fields and give editorial feedback — they don’t replace proper content modeling.

AI search and AEO

The 2026 wrinkle: LLM training and retrieval crawlers (Anthropic, OpenAI, Common Crawl) typically don’t execute JavaScript today, so CSR Portable Text is largely invisible to them regardless of whether Googlebot can render it — but this is provider-specific and not a guaranteed contract. Google’s Gemini pipeline is the notable exception, since it reuses Googlebot’s own rendering infrastructure, and other providers’ behavior can change. Treat “no rendering” as the safe assumption to build for, not a permanent law. Beyond rendering, two Sanity-friendly tactics: serve clean markdown via content negotiation when Accept: text/markdown is present (Sanity’s @portabletext/markdown package converts Portable Text with no extra authoring), and monitor Bing specifically since Bing’s index feeds ChatGPT’s browse responses. On llms.txt, Sanity’s own Knut Melvær is skeptical — “Probably don’t do llms.txtllms.txt is a proposed (not adopted) Markdown file at /llms.txt that gives AI systems a curated map of a site's most important pages. Proposed by Jeremy Howard in 2024, it's read mostly by coding agents like Claude Code — not search crawlers — and Google ignores it. for large sites, because the all-or-nothing scope and file-size limits make it impractical; per-page content negotiation is more granular. Roboto Studio’s summary is the right mindset: “AEO and SEO live on the same plumbing, so we treat them as one job.” (More in AI SearchAI search uses large language models and retrieval-augmented generation (RAG) to synthesize an answer from multiple sources rather than returning a ranked list of links. Examples include Google AI Overviews, ChatGPT Search, and Perplexity..)

The myths worth killing

  • “Sanity does SEO automatically like WordPress + Yoast.” No. Zero HTML, zero automatic anything.
  • “Any rendering mode is fine if the content’s in Sanity.” No. CSR breaks indexingStoring a crawled page in the search index so it can appear in results. Crawled is not the same as indexed — Google selects what to keep, and indexing isn't guaranteed. and AI visibility; dynamic rendering is deprecated.
  • “Drafts can’t be indexed.” They can, if a preview URL leaks. Use a noindex header.
  • “Portable Text is HTML.” It’s a JSON AST that the frontend must serialize.
  • “You need a JSON-LD editor in Studio.” No — generate it from existing fields.

Add an expert note

Pin an expert quote

New person? Create their unclaimed profile at /admin/experts/ → Pin a quote first.