SANITY SEO AUDIT · HEADLESS CMS · PORTABLE TEXT · AI VISIBILITY

Free Sanity.io SEO Audit

Sanity is headless — every SEO outcome depends on the frontend consuming its API. DeepSEOAnalysis audits the rendered output of Sanity-powered sites, detecting Portable Text rendering gaps, GROQ metadata pipeline coverage, sitemap generation, robots.txt configuration, and AI visibility signals — with Next.js, Gatsby, and SvelteKit fix guidance.

Audit my Sanity site →How we score

WHAT WE CHECK

6 Sanity-specific SEO checks

Sanity has no SEO output of its own. The audit evaluates the frontend consuming Sanity — detecting rendering mode (CSR vs SSR vs SSG), Portable Text content rendering in server HTML, GROQ metadata pipeline coverage, sitemap sync via _updatedAt, robots.txt configuration, and AI visibility — with GROQ query patterns and Next.js/Gatsby/SvelteKit component examples for each fix.

⚙️

Headless rendering gap detection

Sanity is a headless CMS that stores structured content and delivers it via API — it has no rendering output. A Next.js App Router frontend with Server Components fetches Sanity content via GROQ server-side and renders complete HTML — Googlebot sees fully populated content on first-wave crawl. A React SPA frontend (Create React App, Vite + React) fetching Sanity client-side delivers an empty HTML shell — Googlebot must wait for the second-wave JavaScript render, which can be delayed days to weeks for non-priority URLs. The audit detects rendering mode by comparing the raw HTTP response (server HTML, pre-JavaScript) against the fully rendered DOM (post-JavaScript). Content in headings, body text, or JSON-LD present in the rendered DOM but absent from the raw server response is flagged as a CSR rendering gap, with guidance on migrating to Next.js Server Components or Gatsby static generation to resolve it.

📝

GROQ-based SEO metadata pipeline

Sanity\'s schema defines document types in TypeScript using `defineType()` and `defineField()`. For SEO metadata, the recommended pattern is a reusable `seoFields` object field (via `defineField({ name: "seo", type: "object", fields: [...] })`) embedded in every public content type, containing `seoTitle` (string), `metaDescription` (string, max 160 chars), `canonicalUrl` (url), `noIndex` (boolean), `ogTitle` (string), `ogDescription` (string), and `ogImage` (image). The frontend queries the SEO fields alongside the document content in a single GROQ query (e.g., `*[_type == "post" && slug.current == $slug][0]{ title, seo { seoTitle, metaDescription, ogImage } }`) and passes them to `generateMetadata()` in Next.js. Common gaps: the `seo` object exists in the schema but content editors leave `metaDescription` empty for most documents (Sanity Studio doesn\'t enforce required fields unless `validation` rules are added); the frontend falls back to the document\'s `title` field for the page title but has no description fallback. The audit checks every crawled page for a unique, non-empty title and meta description in the server-rendered HTML.

🔗

Portable Text rendering and URL structure

Sanity uses Portable Text (a structured JSON format) for rich text content rather than HTML or Markdown. The frontend must render Portable Text to HTML using the `@portabletext/react` (for React/Next.js) or `@portabletext/svelte` library — rendering it client-side means the body content is absent from the server HTML (CSR gap). Server-side rendering of Portable Text in Next.js App Router: use `PortableText` from `@portabletext/react` directly in a Server Component, not in a `use client` component. URL structure is determined by the `slug` field in each Sanity document type — the audit checks that slugs are keyword-descriptive (e.g., `/blog/my-article-title` not `/blog/doc-id-abc123`) and consistent. Common gap: the `slug` field uses `slugify(title)` in Sanity Studio\'s `sanity.document.actions.js`, but if the `title` field uses generic default names (e.g., "Untitled") the slug may not be keyword-rich. The audit flags URL patterns that contain Sanity document ID strings or non-descriptive slugs.

🗺️

Sitemap generation and _updatedAt sync

Sitemaps for Sanity-powered sites must be built by the frontend. In Next.js App Router: `app/sitemap.ts` queries Sanity for all published documents (using `*[_type in ["post","page"] && !(_id in path("drafts.**")) && defined(slug.current)]{ slug, _updatedAt }`) and maps them to sitemap entries. The `_updatedAt` field (ISO 8601 string) is Sanity\'s automatic last-modified timestamp — use it for `lastModified` in the sitemap, which is the correct signal of content recency for Google. For ISR or static builds: configure a Sanity webhook (Studio → API → Webhooks) to trigger a frontend rebuild (Vercel Deploy Hook, Netlify Build Hook) when a document is published or updated — this keeps the sitemap\'s `lastModified` dates accurate. Common gaps: the `Sitemap:` directive in `robots.txt` is missing or points to the wrong URL; draft documents (IDs starting with `drafts.`) are accidentally included in the sitemap; sitemap URLs use the Sanity CDN domain rather than the frontend domain. The audit checks sitemap presence, URL count, `lastmod` recency, and the Sitemap directive in `robots.txt`.

🤖

robots.txt at the frontend domain

robots.txt must be served at the public frontend domain, not at Sanity\'s API domain (`cdn.sanity.io`). In Next.js: `public/robots.txt` (static, served automatically) or `app/robots.ts` returning a `MetadataRoute.Robots` object. In Gatsby: `static/robots.txt`. In SvelteKit: `static/robots.txt`. Common gaps: `robots.txt` is missing at the frontend domain entirely; AI crawlers (GPTBot, ClaudeBot, PerplexityBot, Applebot-Extended) are blocked by a catch-all `Disallow: /`; the `Sitemap:` directive is absent or points to the Sanity CDN URL; Sanity Studio (often hosted at `/studio` or a subdomain) is not disallowed in the `robots.txt` — Studio is an admin interface with authentication and should not be indexed; Sanity preview URLs (if preview mode is enabled at a route like `/preview/*`) are not disallowed. The audit checks all AI crawler user-agents, the Sanity Studio path disallow, and the Sitemap directive.

🧠

AI visibility for Sanity-powered sites

Serve `llms.txt` from the frontend\'s static directory (Next.js `public/llms.txt`, Gatsby `static/llms.txt`, SvelteKit `static/llms.txt`). For FAQPage JSON-LD: add an `faqItems` array to the Sanity document type (each item: `{ question: string, answer: string }`), query it in the GROQ fetch, and render the FAQPage JSON-LD server-side in a Next.js Server Component as `script type="application/ld+json">{JSON.stringify(faqSchema)}</script`. Article JSON-LD for blog posts: build from Sanity fields — `title`, `_createdAt`, `_updatedAt`, `author` reference field (with name), and `seo.ogImage` — injected in the page Server Component or via `generateMetadata()`. The audit checks all five AI visibility signals: llms.txt presence and AI crawler access in robots.txt, FAQPage/HowTo schema in server-rendered HTML, question-heading ratio (≥20%), and content chunkability (avg section ≤400w) — with GROQ query patterns and Next.js/Gatsby component examples for each fix.

HOW IT WORKS

Audit your Sanity site in 60 seconds

  1. Enter your public site URL. Works regardless of frontend — Next.js, Gatsby, SvelteKit, Nuxt, Astro, Remix, or any framework consuming Sanity's Content Lake. Enter the public frontend URL (not cdn.sanity.io or your Studio URL). No Sanity project ID, API tokens, or Studio access needed — the audit crawls the rendered output.
  2. We crawl and analyse. The engine detects the rendering mode (comparing server HTML against post-JavaScript DOM), checks Portable Text body content in the server response, validates metadata in the server-rendered HTML, checks URL slug patterns, validates JSON-LD structured data, tests the sitemap and robots.txt at the frontend domain, measures CrUX Core Web Vitals, and evaluates all five AI visibility signals.
  3. You get a prioritised report. Sanity-specific issues first — rendering gaps, Portable Text CSR rendering, GROQ metadata pipeline, sitemap _updatedAt sync, robots.txt configuration, Studio path disallow — then general SEO, each with GROQ query examples, Sanity schema field patterns, and Next.js/Gatsby/SvelteKit component code for the fix.

FAQ

Questions about the Sanity SEO audit

Does Sanity have built-in SEO features?

Sanity is a headless CMS — it stores structured content in a document store and delivers it via GROQ queries or a CDN-hosted dataset. Sanity has no rendering layer and produces no HTML, title tags, meta descriptions, sitemaps, or structured data on its own. SEO is entirely the responsibility of the frontend framework consuming Sanity. For SEO fields, you create a dedicated `seo` object field in your Sanity schema (containing `title`, `description`, `noIndex`, `ogImage` fields) and include it in every document type that maps to a public URL. The frontend queries these fields via GROQ and injects them into the server-rendered HTML. The Sanity Ecosystem includes community plugins like `sanity-plugin-seo-pane` (adds SEO analysis in the Studio sidebar using Yoast\'s analysis library) and `sanity-plugin-preflight` (schema validation checks), but none of these produce SEO output — that still comes entirely from the frontend.

What frontend framework works best with Sanity for SEO?

Next.js App Router is the most common pairing with Sanity. The `next-sanity` package provides a `createClient()` that works server-side in Next.js Server Components and `generateMetadata()` functions, enabling full SSR: metadata is injected into the server-rendered HTML before JavaScript execution, so Googlebot sees the complete title, description, and Open Graph tags on first-wave crawl. Gatsby with `gatsby-source-sanity` pre-renders all pages to static HTML at build time, querying Sanity\'s dataset at build time via GROQ. Nuxt 3 with `useSanityQuery()` or the `@nuxtjs/sanity` module renders server-side. The key for SEO: the frontend must render metadata into the HTTP response, not add it client-side via `useEffect`. SvelteKit with Sanity can use `load()` server functions to fetch and inject metadata server-side.

How do I generate a sitemap for a Sanity-powered site?

Frontend-generated sitemaps. In Next.js App Router: create `app/sitemap.ts` that queries Sanity using the `createClient()` for all published documents of each public content type (pages, posts, products), maps them to `MetadataRoute.Sitemap` objects with `url` and `lastModified` from the Sanity document\'s `_updatedAt` field, and returns the array. The `_updatedAt` field is automatically maintained by Sanity and reflects when the document was last edited. For Gatsby: `gatsby-plugin-sitemap` uses the Gatsby GraphQL data layer (populated by `gatsby-source-sanity`) to generate the sitemap, with `lastmod` from the `_updatedAt` field. To keep the sitemap current between builds: use Next.js ISR (Incremental Static Regeneration) with `revalidate: 3600` on the sitemap route, or trigger rebuilds via Sanity webhooks (configure in Sanity Studio → API → Webhooks to POST to a Vercel/Netlify deploy hook when documents are published).

Is the Sanity SEO audit free?

Yes. The complete audit is free with no signup and no email gate. Paid plans add saved history, scheduled monitoring, larger crawls (up to 1,000 pages), and agency workflows.