Sanity Best Practices
SanityOfficialPublished by Sanity
- Skill
- Tool integration
- Software development
- MIT
- No issues found
AutoGineer summary
Written by AutoGineer from the skill’s own files, not by the author
This skill provides a comprehensive collection of best practices, architectural patterns, and integration guides for the Sanity ecosystem. It covers the full development lifecycle, including schema design, GROQ query optimization, TypeGen configuration, and framework-specific integrations like Next.js, Angular, and Remix. It also includes guidance on infrastructure management via Blueprints, content automation with Sanity Functions, and building custom applications with the Sanity App SDK.
What it does
- Provides standardized patterns for Sanity schema design and content modeling.
- Offers integration guides for major frontend frameworks and the Sanity App SDK.
- Defines best practices for GROQ query performance and type safety with TypeGen.
- Outlines rules for media handling, including video asset management.
- Details configuration for Sanity Studio structure and Visual Editing.
- Explains infrastructure-as-code workflows using Sanity Blueprints.
When to use it
- When starting a new Sanity project or onboarding team members.
- When integrating Sanity with frameworks like Next.js, Astro, or Angular.
- When optimizing GROQ queries or implementing type-safe data fetching.
- When designing complex content schemas or implementing localization.
- When setting up live previews, Visual Editing, or custom Studio structures.
- When automating content workflows or managing infrastructure via Blueprints.
Advantages
- Maintained by Sanity, ensuring alignment with current platform standards.
- Reduces technical debt by enforcing consistent schema and query patterns.
- Provides clear decision matrices for complex topics like video and localization.
- Includes framework-specific guidance to accelerate frontend integration.
- Promotes maintainable project structures and type-safe development workflows.
Usage examples
- “What are the best practices for handling video in Sanity?”
- The skill advises against using standard file assets for production video due to bandwidth costs, recommending the Sanity Media Library with Mux for Enterprise plans or dedicated services like Mux, YouTube, or Vimeo for others.
- “How do I set up TypeGen for an Angular monorepo?”
- The skill provides a configuration example for studio/sanity.cli.ts to point TypeGen at the Angular source files, ensuring generated types are correctly integrated into the Angular project.
- “What is the recommended way to structure singleton documents?”
- The skill recommends using explicit document IDs for singleton documents and controlling them via the Studio Structure, while letting Sanity generate IDs for all other document types.
- “How should I fetch data in a React app using the App SDK?”
- The skill mandates wrapping data-fetching components in Suspense, using documentId as a React key, and preferring useDocuments and useDocumentProjection over raw GROQ hooks.
Install
Download the skill as a ZIP:
https://mp-staging.autogineer.com/agent-skills/sanity-io/sanity-best-practices/downloadUnzip it into the cross-client skills folder of your project:
mkdir -p .agents/skills && unzip sanity-best-practices.zip -d .agents/skills
Documentation by Sanity
Sanity Best Practices
Comprehensive best practices and integration guides for Sanity development, maintained by Sanity. Use the quick reference below to load only the one or two topic files that match the task.
When to Apply
Reference these guidelines when:
Setting up a new Sanity project or onboarding
Integrating Sanity with a frontend framework (Next.js, Nuxt, Astro, Remix, SvelteKit, Hydrogen)
Writing GROQ queries or optimizing performance
Designing content schemas
Implementing Visual Editing and live preview
Working with images, Portable Text, or page builders
Configuring Sanity Studio structure
Setting up TypeGen for type safety
Implementing localization
Migrating content from other systems
Building custom apps with the Sanity App SDK
Developing or deploying a Studio or app in the Sanity Dashboard
Managing infrastructure with Blueprints
Automating content workflows with Sanity Functions or webhooks
Global Rules
Let Sanity generate
_idvalues for ordinary documents. Do not create deterministic UUIDs, slug-derived IDs, or legacy-system IDs when creating documents.Model relationships with
referencefields, then resolve related documents with GROQ lookups, source-key fields, or returned_idvalues from created documents.Use explicit document IDs mainly for singleton documents controlled by Studio Structure, including localized singletons such as
homePage-en.
Video
Do not store or serve video from Sanity
fileassets for production playback. File assets are delivered as raw downloads with no transcoding or adaptive streaming, and video traffic drives very high bandwidth usage and unexpectedly large bills.On Enterprise plans with the video add-on, use Sanity Media Library for video: uploads are transcoded and streamed adaptively via Mux. Model video fields with
defineVideoField()fromsanity/media-libraryand play them with@mux/mux-player-reactusing the asset's playback ID.On other plans, use a dedicated video service: install
sanity-plugin-mux-inputto upload and manage videos in your Mux account from the Studio, or host video on a platform such as YouTube or Vimeo and store only the embed URL in Sanity.Small clips and short previews in a
filefield are acceptable, but any user-facing video at scale must go through Media Library or a streaming service.
Quick Reference
Integration Guides
get-started- Interactive onboarding for new Sanity projectsnextjs- Next.js App Router, Live Content API, standalone Studionuxt- Nuxt integration with @nuxtjs/sanityangular- Angular integration with @sanity/client, signals, resource APIastro- Astro integration with @sanity/astroremix- React Router / Remix integrationsvelte- SvelteKit integration with @sanity/svelte-loaderhydrogen- Shopify Hydrogen with Sanityproject-structure- Standalone Studio and monorepo patternsapp-sdk- Custom applications with Sanity App SDKdashboard- Studios and apps in the Sanity Dashboard: defineApplication, local dev, deploys, Dashboard hooksblueprints- Infrastructure as Code: blueprint files, stacks, plan/deploy workflow, error recovery, CI deploysfunctions- Automating content workflows with Sanity Functions and webhooks
Topic Guides
groq- GROQ query patterns, type safety, performance optimizationschema- Schema design, field definitions, validation, deprecation patternsvisual-editing- Presentation Tool, Stega, overlays, live previewpage-builder- Page Builder arrays, block components, live editingportable-text- Rich text rendering and custom componentsimage- Image schema, URL builder, hotspots, LQIP, Next.js Imagestudio-structure- Desk structure, singletons, navigationtypegen- TypeGen configuration, workflow, type utilitiesseo- Metadata, sitemaps, Open Graph, JSON-LDlocalization- i18n patterns, document vs field-level, locale managementmigration- Content import overview (see alsomigration-html-import)migration-html-import- HTML to Portable Text with @portabletext/block-tools
How to Use
Start with the single framework or topic guide that best matches the request, then read additional references only when the task crosses concerns. Use these reference files for detailed explanations and code examples:
references/groq.md
references/schema.md
references/nextjs.mdEach reference file contains:
Comprehensive topic or integration coverage
Incorrect and correct code examples
Decision matrices and workflow guidance
Framework-specific patterns where applicable
Files
- ATTRIBUTION.mdAdded by AutoGineer873 B
- LICENSELicence file added by AutoGineer1.0 KiB
- SKILL.md5.4 KiB
- references/angular.md18.6 KiB
- references/app-sdk.md12.0 KiB
- references/astro.md4.9 KiB
- references/blueprints.md14.1 KiB
- references/dashboard.md6.5 KiB
- references/functions.md29.7 KiB
- references/get-started.md21.0 KiB
- references/groq.md14.4 KiB
- references/hydrogen.md6.8 KiB
- references/image.md3.3 KiB
- references/localization.md12.0 KiB
- references/migration-html-import.md5.2 KiB
- references/migration.md2.7 KiB
- references/nextjs.md16.3 KiB
- references/nuxt.md5.9 KiB
- references/page-builder.md8.2 KiB
- references/portable-text.md9.8 KiB
- references/project-structure.md3.5 KiB
- references/remix.md8.8 KiB
- references/schema.md12.4 KiB
- references/seo.md8.3 KiB
- references/studio-structure.md4.0 KiB
- references/svelte.md10.0 KiB
- references/typegen.md6.1 KiB
- references/visual-editing.md8.4 KiB
references/nextjs.md
title: Next.js & Sanity Integration Rules description: Integration guide for Next.js App Router, Live Content API, and a standalone Sanity Studio.
Next.js & Sanity Integration Rules
Jump to the section that matches the task instead of reading this guide end-to-end.
Table of Contents
Architecture patterns
Data fetching (Live Content API)
Caching and revalidation
Visual Editing and clean data
Studio setup (standalone)
Draft Mode setup
Error handling
Presentation queries
Pagination pattern
1. Architecture Patterns
Option A: Standalone Studio (Recommended)
Best for: All new Next.js projects.
The Studio is its own app, living alongside the Next.js app in the same repo:
your-project/
├── studio/ # Sanity Studio (standalone)
└── web/ # Next.js frontendWhy standalone instead of embedding the Studio in the Next.js app:
Faster dev and builds:
sanity devandsanity buildrun on Vite and are dramatically faster (10-30x) than compiling the Studio throughnext dev/next build.Auto-updates: Standalone Studios receive bugfixes and new features automatically, with no dependency bump or redeploy. Embedded Studios can't auto-update (Next.js does not support ESM with import maps), so every update means bump + deploy.
TypeGen watch mode: With
sanity dev, TypeGen regenerates types as queries change. Embedded Studios can't hook intonext dev, so you must re-runsanity typegen generatemanually after every query edit.Content model independence: A separate Studio keeps the content model from becoming website-centric and makes collaboration easier.
Setup:
Run both apps side by side in separate terminals:
next dev(localhost:3000) andsanity dev(localhost:3333).Add your Next.js app URL to CORS Origins:
npx sanity cors add http://localhost:3000 --credentials(repeat for your production URL), or via Sanity Manage.See
project-structure.mdrule for detailed structure.
Option B: Embedded Studio (Not Recommended)
The Studio can be mounted inside the Next.js app at /app/studio/[[...tool]]/page.tsx via next-sanity/studio. Avoid this for new projects: it slows builds, ties every Studio update to an app deploy, and rules out auto-updates and TypeGen watch mode. For maintaining or migrating an existing embedded Studio, see section 5.
2. Data Fetching (Live Content API)
We use defineLive (next-sanity v11+) to enable real-time content updates and Visual Editing automatically.
Setup (src/sanity/lib/live.ts)
import { defineLive } from 'next-sanity/live'
import { client } from './client'
export const { sanityFetch, SanityLive } = defineLive({
client: client.withConfig({
apiVersion: '2026-02-01'
}),
serverToken: process.env.SANITY_API_READ_TOKEN,
browserToken: process.env.SANITY_API_READ_TOKEN,
})Rendering (src/app/layout.tsx)
You must render <SanityLive /> in the root layout to enable real-time updates.
import { SanityLive } from '@/sanity/lib/live'
import { VisualEditing } from 'next-sanity/visual-editing'
import { draftMode } from 'next/headers'
export default async function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
{children}
<SanityLive />
{(await draftMode()).isEnabled && <VisualEditing />}
</body>
</html>
)
}3. Caching & Revalidation
Prefer Live Content API (Default)
Use defineLive by default. It handles fetching, caching, and invalidation automatically. Only implement manual caching when you need fine-grained control.
When to Use Manual Caching
| Scenario | Approach |
|---|---|
| Real-time updates, Visual Editing | defineLive (default) |
| Static marketing pages, rarely updated | Time-based revalidation |
| Blog posts, products with frequent edits | Tag-based revalidation |
| Critical accuracy (stock levels, prices) | Path-based + short revalidation |
Debugging: Enable Fetch Logging
See every fetch with cache HIT/MISS status:
// next.config.ts
const nextConfig: NextConfig = {
logging: {
fetches: {
fullUrl: true,
},
},
};Console output shows cache status:
GET /posts 200 in 39ms
│ GET https://...apicdn.sanity.io/... 200 in 5ms (cache hit)Sanity CDN vs API
| Setting | Speed | Freshness | Use When |
|---|---|---|---|
useCdn: true | Fast | May have brief delay | Default for all runtime fetches |
useCdn: false | Slower | Guaranteed fresh | generateStaticParams, webhooks |
Override per-request:
// For static generation, use API directly
export async function generateStaticParams() {
const slugs = await client
.withConfig({ useCdn: false })
.fetch(SLUGS_QUERY);
return slugs;
}Manual sanityFetch Helper (Advanced)
For manual caching control, create a wrapper:
// src/sanity/lib/client.ts
export async function sanityFetch<const QueryString extends string>({
query,
params = {},
revalidate = 60,
tags = [],
}: {
query: QueryString;
params?: QueryParams;
revalidate?: number | false;
tags?: string[];
}) {
return client.fetch(query, params, {
next: {
revalidate: tags.length ? false : revalidate,
tags,
},
});
}Time-Based Revalidation
Simple and predictable. Good for content that changes infrequently.
const posts = await sanityFetch({
query: POSTS_QUERY,
revalidate: 3600, // Revalidate every hour
});The "Typo Problem": With time-based only, content authors may wait up to an hour to see changes. Use webhooks for instant updates.
Path-Based Revalidation
Surgically revalidate specific routes when documents change.
1. Create API Route:
// src/app/api/revalidate/path/route.ts
import { revalidatePath } from 'next/cache';
import { type NextRequest, NextResponse } from 'next/server';
import { parseBody } from 'next-sanity/webhook';
type WebhookPayload = { path?: string };
export async function POST(req: NextRequest) {
try {
const { isValidSignature, body } = await parseBody<WebhookPayload>(
req,
process.env.SANITY_REVALIDATE_SECRET,
true // Add delay to allow CDN to update
);
if (!isValidSignature) {
return new Response('Invalid signature', { status: 401 });
}
if (!body?.path) {
return new Response('Missing path', { status: 400 });
}
revalidatePath(body.path);
return NextResponse.json({ revalidated: body.path });
} catch (err) {
return new Response((err as Error).message, { status: 500 });
}
}2. Create GROQ-Powered Webhook:
URL:
https://yoursite.com/api/revalidate/pathFilter:
_type in ["post"]Projection:
{ "path": "/posts/" + slug.current }Add
SANITY_REVALIDATE_SECRETto webhook and.env.local
Tag-Based Revalidation
"Update once, revalidate everywhere" — best for referenced content.
1. Tag Your Queries:
// Posts index - revalidate when ANY post, author, or category changes
const posts = await sanityFetch({
query: POSTS_QUERY,
tags: ['post', 'author', 'category'],
});
// Individual post - more granular, includes slug-specific tag
const post = await sanityFetch({
query: POST_QUERY,
params,
tags: [`post:${params.slug}`, 'author', 'category'],
});2. Create API Route:
// src/app/api/revalidate/tag/route.ts
import { revalidateTag } from 'next/cache';
import { type NextRequest, NextResponse } from 'next/server';
import { parseBody } from 'next-sanity/webhook';
type WebhookPayload = { tags: string[] };
export async function POST(req: NextRequest) {
try {
const { isValidSignature, body } = await parseBody<WebhookPayload>(
req,
process.env.SANITY_REVALIDATE_SECRET,
true
);
if (!isValidSignature) {
return new Response('Invalid signature', { status: 401 });
}
if (!Array.isArray(body?.tags) || !body.tags.length) {
return new Response('Missing tags', { status: 400 });
}
body.tags.forEach((tag) => revalidateTag(tag));
return NextResponse.json({ revalidated: body.tags });
} catch (err) {
return new Response((err as Error).message, { status: 500 });
}
}3. Create GROQ-Powered Webhook:
URL:
https://yoursite.com/api/revalidate/tagFilter:
_type in ["post", "author", "category"]Projection:
{ "tags": [_type, _type + ":" + slug.current] }
Stale Data After Webhook?
Webhooks fire before Sanity CDN updates. If you see stale data:
Add delay — Pass
trueas third arg toparseBodyOr bypass CDN — Set
useCdn: falsein client config (use sparingly)
4. Visual Editing (Stega) & Clean Data
Visual Editing injects invisible characters into strings to enable click-to-edit.
A. The Golden Rule of Stega
If a string field controls logic (alignment, colors, IDs), you must clean it before comparing.
import { stegaClean } from "@sanity/client/stega";
export function Layout({ align }: { align: string }) {
// ❌ Bad: Will fail in Edit Mode due to invisible chars
// if (align === 'center') ...
// ✅ Good: Clean the value first
const cleanAlign = stegaClean(align);
return <div className={cleanAlign === 'center' ? 'mx-auto' : ''} />
}B. Metadata & SEO (Critical)
Never let Stega characters leak into <head> tags. Always set stega: false for metadata fetching.
export async function generateMetadata({ params }) {
const { data } = await sanityFetch({
query: SEO_QUERY,
params: await params,
stega: false // 👈 Critical for SEO
})
return { title: data?.title }
}C. Static Params
When generating static params, fetch only published content and disable stega.
export async function generateStaticParams() {
const { data } = await sanityFetch({
query: SLUGS_QUERY,
perspective: 'published', // 👈 No drafts
stega: false
})
return data
}5. Setup: Studio (Standalone)
Create the Studio as its own app from the repo root — not inside the Next.js app folder, where the CLI would switch to its embedded flow:
npm create sanity@latest -- --project <projectId> --dataset production --template clean --typescript --output-path studioRun it with npm run dev inside studio/ (defaults to http://localhost:3333). For Visual Editing, point the Presentation Tool's previewUrl.initial at the Next.js app (see visual-editing.md).
Migrating an Existing Embedded Studio
Embedded Studios (<NextStudio /> mounted at a route like /app/studio/[[...tool]]/page.tsx) keep working, but migrating to a standalone Studio is recommended:
Create a standalone Studio folder as above, reusing your existing
projectIdand dataset.Move
sanity.config.ts,sanity.cli.ts, and your schema types into it.Delete the
/app/studio/[[...tool]]/route from the Next.js app. Keepnext-sanity— the app still needs it for fetching, Live Content, and Visual Editing.Add the app's URLs to CORS origins and set the Presentation Tool's
previewUrl.initialto the app's URL.
6. Setup: Draft Mode
Enable Presentation Tool and Visual Editing by setting up a draft mode route.
src/app/api/draft-mode/enable/route.ts:
import { client } from '@/sanity/lib/client'
import { defineEnableDraftMode } from 'next-sanity/draft-mode'
import { token } from '@/sanity/lib/token' // Helper to get token
export const { GET } = defineEnableDraftMode({
client: client.withConfig({ token }),
})7. Error Handling
Use notFound() for missing documents. Common errors:
| Error | Cause | Solution |
|---|---|---|
| 401 Unauthorized | Invalid/missing token | Check SANITY_API_READ_TOKEN |
| 403 Forbidden | CORS not configured | Add URL to CORS origins |
| Query syntax error | Invalid GROQ | Test in Vision plugin first |
| Empty result | Wrong filter/params | Log params, check _type spelling |
import { notFound } from 'next/navigation'
export default async function PostPage({ params }: Props) {
const { data } = await sanityFetch({ query: POST_QUERY, params: await params })
if (!data) notFound()
return <Post data={data} />
}8. Presentation Queries (usePresentationQuery)
For faster live editing in the Presentation Tool, use usePresentationQuery to fetch only the specific block being edited, rather than re-rendering the entire page.
Why Use This
Without: Editing a hero title re-fetches the whole page, re-renders all blocks
With: Only the hero block re-fetches and re-renders
This is especially valuable for pages with many Page Builder blocks or complex Portable Text.
Basic Pattern
'use client'
import { usePresentationQuery } from 'next-sanity/hooks'
import { HERO_PRESENTATION_QUERY } from '@/sanity/lib/queries'
type HeroProps = {
_key: string
documentId: string
title: string
subtitle?: string
// ... other initial props from page query
}
export function Hero({ _key, documentId, title, subtitle, ...rest }: HeroProps) {
// Fetch block-specific data for faster updates in Presentation Tool
const { data } = usePresentationQuery({
query: HERO_PRESENTATION_QUERY,
params: { documentId, blockKey: _key },
})
// Use presentation data if available, fallback to initial server props
const blockData = data?.heroBlock || { title, subtitle, ...rest }
return (
<section>
<h1>{blockData.title}</h1>
{blockData.subtitle && <p>{blockData.subtitle}</p>}
</section>
)
}The Presentation Query
Create a query that targets the specific block by _key:
// queries.ts
export const HERO_PRESENTATION_QUERY = defineQuery(`
*[_id == $documentId][0]{
_id,
_type,
"heroBlock": pageBuilder[_key == $blockKey && _type == "hero"][0]{
title,
subtitle,
image,
theme,
// Include all fields the component needs
}
}
`)Passing Document Context
Your PageBuilder component needs to pass documentId to each block:
export function PageBuilder({ content, documentId }: { content: Block[]; documentId: string }) {
return (
<main>
{content.map((block) => {
switch (block._type) {
case "hero":
return <Hero key={block._key} documentId={documentId} {...block} />
// ... other blocks
}
})}
</main>
)
}For Portable Text Blocks
The same pattern works for custom blocks inside Portable Text:
export const PTE_IMAGE_PRESENTATION_QUERY = defineQuery(`
*[_id == $documentId][0]{
"pteImageBlock": body[_key == $blockKey && _type == "pteImage"][0]{
image,
caption,
alt
}
}
`)See also: visual-editing.md for the conceptual overview and page-builder.md for full Page Builder patterns.
9. Pagination Pattern
For listing pages with many entries, use offset-based pagination with a count query.
GROQ slice bounds ([start...end]) must be constant numbers, not $params. Validate the page bounds in application code and interpolate them directly into the query string.
Queries
// Total count for pagination UI
export const ARTICLES_COUNT_QUERY = defineQuery(`
count(*[_type == "article" && defined(slug.current)])
`);
// Paginated listing — validated integers interpolated into the slice
function articlesQuery(start: number, end: number) {
return defineQuery(`
*[_type == "article" && defined(slug.current)]
| order(date desc) [${start}...${end}] {
_id, title, "slug": slug.current, date
}
`);
}Listing Page
const ENTRIES_PER_PAGE = 10;
export default async function BlogPage({
searchParams
}: {
searchParams: Promise<{ page?: string }>
}) {
const { page: pageParam } = await searchParams;
const page = Math.max(1, parseInt(pageParam || "1") || 1);
const start = (page - 1) * ENTRIES_PER_PAGE;
const end = start + ENTRIES_PER_PAGE;
const [{ data: articles }, { data: total }] = await Promise.all([
sanityFetch({ query: articlesQuery(start, end) }),
sanityFetch({ query: ARTICLES_COUNT_QUERY })
]);
const totalPages = Math.ceil(total / ENTRIES_PER_PAGE);
return (
<main>
{articles.map(article => (
<ArticleCard key={article._id} article={article} />
))}
<Pagination current={page} total={totalPages} />
</main>
);
}Requirements and compatibility
Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta
“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.
Security
No issues found by our static scan· No issues found
A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.
Attribution and licence
- Published by
- Sanity
- Publisher
- Sanity (Official)
- Source
- sanity-io/agent-toolkit · skills/sanity-best-practices · commit 3ed3a14
- Licence
- MIT License (redistribution allowed)
- LICENSE (added by AutoGineer from LICENSE)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
AutoGineer is not affiliated with or endorsed by Sanity.
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download sanity-best-practices.zip