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://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/schema.md
title: Sanity Schema Best Practices description: Rules for defining Sanity Content Models (Schemas), including field definitions, strict typing, and validation patterns.
Sanity Schema Best Practices
Use this contents list to jump to the schema design decision you are making.
Table of Contents
Core philosophy: data over presentation
Strict definition syntax
Shared fields pattern
Field patterns
References vs nested objects
Document creation and IDs
Safe schema updates
Validation patterns
1. Core Philosophy: Data > Presentation
Model what things are, not what they look like.
❌ Bad:
bigHeroText,redButton,threeColumnRow,color,fontSize✅ Good:
heroStatement,callToAction,featuresSection,status,role
The test: "If we redesigned the site, would this field name still make sense?"
threeColumnLayout→ ❌ Fails (what if we go to 2 columns?)features→ ✅ Passes (features are features regardless of layout)
2. Strict Definition Syntax
Always use the helper functions from sanity for type safety and autocompletion.
ALWAYS use
defineTypefor the root export.ALWAYS use
defineFieldfor fields.ALWAYS use
defineArrayMemberfor items inside arrays.OMIT
titlewhen it only repeats thename. It is optional and defaults to the title-casedname(mainImage→ "Main Image"), so never writename: 'author', title: 'Author'. Set it only when it adds something: acronyms ('SEO'), clarification ('Alternative Text'foralt), or notes like'(Deprecated)'.
import { defineType, defineField, defineArrayMember } from 'sanity'
import { TagIcon } from '@sanity/icons/Tag'
export const article = defineType({
name: 'article',
type: 'document',
icon: TagIcon,
fields: [
defineField({
name: 'title',
type: 'string',
validation: (rule) => rule.required(),
}),
defineField({
name: 'tags',
type: 'array',
of: [
// ALWAYS use defineArrayMember for array items
defineArrayMember({ type: 'reference', to: [{ type: 'tag' }] })
]
})
]
})3. Shared Fields Pattern
Export arrays of fields to reuse common patterns (e.g., SEO, standard page headers).
// src/schemaTypes/shared/seoFields.ts
export const seoFields = [
defineField({ name: 'seoTitle', type: 'string', title: 'SEO Title' }),
defineField({ name: 'seoDesc', type: 'text', title: 'SEO Description' })
]
// Usage
defineType({
name: 'page',
fields: [
defineField({ name: 'title', type: 'string' }),
...seoFields // Spread shared fields
]
})4. Field Patterns
A. Array Keys (_key)
Every item in a Sanity array automatically gets a _key property. This is critical for:
React reconciliation (use as
keyprop)Visual Editing overlays (click-to-edit)
Portable Text rendering
Schema: Sanity auto-generates _key for array items. You don't define it.
Frontend: Always use _key as React's key:
// ✅ Correct
{items.map((item) => <Component key={item._key} {...item} />)}
// ❌ Wrong - index keys break Visual Editing
{items.map((item, i) => <Component key={i} {...item} />)}Querying: Always include _key in array projections:
*[_type == "page"][0]{
pageBuilder[]{
_key, // Always include _key in queries
_type,
...
}
}B. Icons
Always assign an icon from @sanity/icons to documents and objects. This improves the Studio UX significantly. Browse all icons at icons.sanity.build.
// ✅ Correct — import each icon from its own subpath
import { DocumentTextIcon } from '@sanity/icons/DocumentText'
// ❌ Wrong — root named exports were removed in v5.
// Type-checks clean, then fails at bundle time.
import { DocumentTextIcon } from '@sanity/icons'| Content Type | Icon | Import |
|---|---|---|
| Article, Post | DocumentTextIcon | @sanity/icons/DocumentText |
| Author, Person | UserIcon | @sanity/icons/User |
| Category, Tag | TagIcon | @sanity/icons/Tag |
| Settings | CogIcon | @sanity/icons/Cog |
| Page | DocumentIcon | @sanity/icons/Document |
| Image block | ImageIcon | @sanity/icons/Image |
| Video block | PlayIcon | @sanity/icons/Play |
| FAQ | HelpCircleIcon | @sanity/icons/HelpCircle |
| Link | LinkIcon | @sanity/icons/Link |
C. Boolean vs. List
Avoid boolean fields for binary states that might expand later.
Prefer:
options.listwith "radio" layout.
defineField({
name: 'status',
type: 'string',
options: {
list: [
{ title: 'Draft', value: 'draft' },
{ title: 'Published', value: 'published' }
],
layout: 'radio'
}
})D. The "Toggle" Pattern (Conditional Fields)
Use a radio/boolean field to toggle visibility of other fields (often grouped in fieldsets).
defineField({
name: 'linkType',
type: 'string',
options: { list: ['internal', 'external'], layout: 'radio' }
}),
defineField({
name: 'internalLink',
type: 'reference',
hidden: ({ parent }) => parent?.linkType !== 'internal'
}),
defineField({
name: 'externalUrl',
type: 'url',
hidden: ({ parent }) => parent?.linkType !== 'external'
})5. References vs Nested Objects
A critical modeling decision: when to use reference vs embedding an object.
Use References When:
Content is reusable across documents (authors, categories, products)
Content needs its own editing interface in Studio
You need to query/filter by the related content independently
Multiple documents should share the same instance (update once, reflect everywhere)
// ✅ Author is reusable and independently editable
defineField({
name: 'author',
type: 'reference',
to: [{ type: 'author' }]
})Use Nested Objects When:
Content is specific to this document (not shared)
Content doesn't make sense on its own (address, SEO metadata)
You want simpler editing (all fields in one place)
You need the data to be copied not linked
// ✅ SEO is document-specific, not shared
defineField({
name: 'seo',
type: 'object',
fields: [
defineField({ name: 'title', type: 'string' }),
defineField({ name: 'description', type: 'text' })
]
})Quick Decision Matrix
| Scenario | Use |
|---|---|
| Blog post author | reference (reusable) |
| Product category | reference (shared taxonomy) |
| Page SEO fields | object (page-specific) |
| Hero section content | object (page-specific) |
| Team member on About page | reference (might be used elsewhere) |
| Call-to-action button | object (usually page-specific) |
Querying Differences
// Reference requires expansion
*[_type == "post"]{ author->{ name, bio } }
// Object is already inline
*[_type == "post"]{ seo { title, description } }6. Document Creation and IDs
Sanity document _id values are implementation identifiers, not a content modeling tool.
Prefer generated IDs: Let Sanity assign
_idvalues for ordinary content documents. Avoid deterministic UUIDs, slug-derived IDs, and IDs copied from legacy systems.Use relationships, not ID conventions: Connect documents with
referencefields and set_reffrom an actual lookup or from the_idreturned after creating the related document.Store source identity as content: For imports, put legacy IDs, external IDs, or stable slugs in explicit fields such as
legacyId,externalId, orslug, then query by those fields when you need to find or upsert content.Keep explicit IDs rare: Directly setting
_idis mainly useful for singleton documents managed through Studio Structure, such assettingsor localized singletons likehomePage-en.
// ✅ Correct - relationship comes from a lookup
import {defineQuery} from 'groq'
const AUTHOR_BY_EXTERNAL_ID_QUERY = defineQuery(`
*[_type == "author" && externalId == $externalId][0]{_id}
`)
const author = await client.fetch(AUTHOR_BY_EXTERNAL_ID_QUERY, {
externalId: post.authorId,
})
if (!author?._id) throw new Error(`Missing author for ${post.authorId}`)
await client.create({
_type: 'post',
title: post.title,
slug: {_type: 'slug', current: post.slug},
legacyId: post.id,
author: {_type: 'reference', _ref: author._id},
})
// ❌ Wrong - IDs encode relationships and source data
await client.createOrReplace({
_id: `post-${post.id}`,
_type: 'post',
author: {_type: 'reference', _ref: `author-${post.authorId}`},
})7. Safe Schema Updates (The Deprecation Pattern)
NEVER delete a field that contains production data. It will cause data loss or Studio crashes. Instead, follow the ReadOnly -> Hidden -> Deprecated lifecycle.
The Pattern
deprecated: Adds a visual warning and reason.readOnly: true: Prevents new edits but keeps data visible.hidden: Hides it from new documents (where value is undefined).initialValue: undefined: Ensures new documents don't get this field.
defineField({
name: 'oldTitle', // The field you want to remove
title: 'Article Title (Deprecated)',
type: 'string',
deprecated: {
reason: 'Use the new "seoTitle" field instead. This will be removed in v2.'
},
readOnly: true,
hidden: ({ value }) => value === undefined,
initialValue: undefined
})Migration Workflow
Phase 1: Deprecate — Apply the deprecation pattern above. Deploy.
Phase 2: Migrate — Update frontend to use new fields (with coalesce() fallbacks). Create a migration:
// migrations/rename-oldTitle-to-newTitle/index.ts
import {defineMigration, at, setIfMissing, unset} from 'sanity/migrate'
export default defineMigration({
title: 'Rename oldTitle to newTitle',
documentTypes: ['article'],
filter: 'defined(oldTitle) && !defined(newTitle)',
migrate: {
document(doc) {
if (!doc.oldTitle || doc.newTitle) return
return [
at('newTitle', setIfMissing(doc.oldTitle)),
at('oldTitle', unset())
]
}
}
})# Dry run first (default)
sanity migrations run rename-oldTitle-to-newTitle
# Execute when ready
sanity migrations run rename-oldTitle-to-newTitle --no-dry-runPhase 3: Remove — Once oldTitle is undefined for all documents, delete the field definition.
8. Validation Patterns
Beyond rule.required(), Sanity offers powerful validation options.
Common Patterns
// Email validation
defineField({
name: 'email',
type: 'string',
validation: (rule) => rule.email().required()
})
// URL validation (with custom message)
defineField({
name: 'website',
type: 'url',
validation: (rule) => rule.uri({
scheme: ['http', 'https']
}).error('Must be a valid URL starting with http:// or https://')
})
// Length constraints
defineField({
name: 'excerpt',
type: 'text',
validation: (rule) => rule.max(200).warning('Keep it under 200 characters for best SEO')
})
// Regex pattern
defineField({
name: 'slug',
type: 'slug',
validation: (rule) => rule.required().custom((slug) => {
if (!slug?.current) return 'Required'
if (!/^[a-z0-9-]+$/.test(slug.current)) {
return 'Slug must be lowercase with hyphens only'
}
return true
})
})Cross-Field Validation
defineField({
name: 'endDate',
type: 'datetime',
validation: (rule) => rule.custom((endDate, context) => {
const startDate = context.document?.startDate
if (startDate && endDate && new Date(endDate) < new Date(startDate)) {
return 'End date must be after start date'
}
return true
})
})Array Validation
defineField({
name: 'tags',
type: 'array',
of: [{ type: 'string' }],
validation: (rule) => rule
.min(1).error('Add at least one tag')
.max(10).warning('Too many tags may hurt SEO')
.unique()
})Async Validation (Uniqueness Check)
defineField({
name: 'slug',
type: 'slug',
validation: (rule) => rule.required().custom(async (slug, context) => {
if (!slug?.current) return true
const client = context.getClient({ apiVersion: '2026-02-01' })
const id = context.document?._id?.replace(/^drafts\./, '')
const existing = await client.fetch(
`count(*[_type == "post" && slug.current == $slug && _id != $id])`,
{ slug: slug.current, id }
)
return existing === 0 || 'Slug already exists'
})
})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 or Sanity.
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download sanity-best-practices.zip