Sanity Setup
Environment model
One Sanity project, one dataset:production. There is no staging dataset.
- Production deployment (
VERCEL_ENV=production): tokenless reads,publishedperspective, Sanity CDN — only published articles, fully public for SEO/AEO. - Every other deployment (previews, the staging site, local dev): reads
the same
productiondataset with a viewer token and thedraftsperspective — published articles plus drafts, with drafts tagged in the UI (card badge + article banner). Drafts stay invisible to the public because they require the token.
NEXT_PUBLIC_SANITY_DATASET is no longer read; remove it from Vercel env.
Required environment variables
NEXT_PUBLIC_SANITY_PROJECT_IDNEXT_PUBLIC_SANITY_API_VERSIONSANITY_REVALIDATE_SECRETSANITY_PREVIEW_READ_TOKEN(legacy nameSANITY_STAGING_READ_TOKENalso works) — viewer token for non-production draft previewsINDEXNOW_KEY— search-engine ping on publish
Files
studio/sanity.config.tsconfigures the standalone Sanity Studiostudio/sanity.cli.tsconfigures CLI API defaultsstudio/schemaTypes/authorType.tsstudio/schemaTypes/categoryType.tsstudio/schemaTypes/postType.ts
studio/ directory with npm run dev, validate it with npm run build, and
deploy it with npm run deploy. It has a single workspace on the
production dataset — the dataset is hardcoded on purpose so a deploy can
never silently point the Studio somewhere else. Drafts saved here are the
preview content for non-production deployments.
The post body schema supports text, images, dividers, tables, and a
callToAction object with a heading, description, trust line, button label,
and absolute button URL.
Posts also support a faq array (question + plain-text answer), authored in
the Studio. It renders as a visible FAQ section at the end of the article and
as FAQPage JSON-LD.
Public readability (SEO/AEO)
The blog is intentionally fully public: theproduction dataset is public, the
production site reads without any token, and search/answer-engine crawlers
must be able to see everything. Two rules keep it that way:
- Blog document ids must never contain a dot. Sanity hides dotted “path” ids
(
post.<slug>) from unauthenticated requests even on public datasets — publishing with dotted ids silently blanks the public blog. Usepost-<slug>/category-<slug>/author-<slug>, or let Sanity generate plain ids (Studio-created documents are safe by default). - Tokenless reads go through Sanity’s CDN (
useCdnis enabled when no read token is configured) for faster public TTFB; non-production deployments use a viewer token + thedraftsperspective on the live API so drafts appear immediately.
Revalidation webhook
The app exposesPOST /api/revalidate/sanity.
Two authentication forms, each sufficient:
- Sanity webhook deliveries: a valid
sanity-webhook-signatureheader, signed withSANITY_REVALIDATE_SECRET(set the same value in the webhook’s Secret field in manage.sanity.io — Sanity signs every delivery automatically, with replay-bounded timestamps). - Manual/ops calls:
Authorization: Bearer <SANITY_REVALIDATE_SECRET>.
- Revalidates blog listing cache tags
- Revalidates blog post tag for the provided slug
- Revalidates
/blog - Revalidates
/blog/[slug]when slug is present - Revalidates
/sitemap.xml - Submits the article URL to IndexNow (best-effort) when the slug resolves to a published, indexable post
SANITY_REVALIDATE_SECRET in the webhook’s Secret field and a
{"slug": slug.current} projection — with publishing happening directly in
Sanity, this webhook is the only post-publish hook. Duplicate deliveries are
harmless: revalidation and the IndexNow ping are both idempotent.
Authoring and publishing
Content is authored directly in Sanity — there is no git-side pipeline. Agents create drafts via the Sanity MCP (or API) following.agents/skills/author-blog-posts/SKILL.md; humans review and publish in the
Studio. The publish webhook above handles revalidation and IndexNow.
Redesign data contract
The directory pills are read fromcategory documents and post cards resolve
categories[] references. The Framer sample categories are not imported.
The redesign requires no schema migration or content writes. Authors, hero
images, body blocks and FAQs continue using the existing Studio fields.
Author linkedinUrl and xUrl fields render as profile links in the article
byline; empty fields do not create social controls.
getBlogRelatedPosts(slug) searches all eligible articles, excluding that
slug, and returns up to three ranked by shared category references, newest
publication date, then document ID. It uses the same published/draft
perspective and cache tags as other blog reads.