Generated banner for Sanity Best Practices
Generated logo for Sanity Best Practices

Sanity Best Practices

SanityOfficialPublished by Sanity

  • Skill
  • Tool integration
  • Software development
  • MIT
  • No issues found

From the author

Sanity development best practices for schema design, GROQ queries, TypeGen, Visual Editing, images, Portable Text, Studio structure, localization, migrations, Sanity Functions, webhooks, Blueprints, and framework integrations such as Next.js, Nuxt, Astro, Remix, SvelteKit, Angular, Hydrogen, the App SDK, and Studios or apps running in the Sanity Dashboard (defineApplication). Use this skill whenever working with Sanity schemas, defineType or defineField, GROQ or defineQuery, content modeling, Presentation or preview setups, Sanity-powered frontend integrations, event-driven content automation, documentEventHandler, defineDocumentFunction, defineMediaLibraryAssetFunction, @sanity/functions, @sanity/blueprints, sanity.blueprint.ts, event-driven content automation, or when reviewing and fixing a Sanity codebase.
— the description by Sanity

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

  1. Download the skill as a ZIP:

    https://autogineer.com/agent-skills/sanity-io/sanity-best-practices/download
  2. Unzip 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 _id values for ordinary documents. Do not create deterministic UUIDs, slug-derived IDs, or legacy-system IDs when creating documents.

  • Model relationships with reference fields, then resolve related documents with GROQ lookups, source-key fields, or returned _id values 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 file assets 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() from sanity/media-library and play them with @mux/mux-player-react using the asset's playback ID.

  • On other plans, use a dedicated video service: install sanity-plugin-mux-input to 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 file field 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 projects

  • nextjs - Next.js App Router, Live Content API, standalone Studio

  • nuxt - Nuxt integration with @nuxtjs/sanity

  • angular - Angular integration with @sanity/client, signals, resource API

  • astro - Astro integration with @sanity/astro

  • remix - React Router / Remix integration

  • svelte - SvelteKit integration with @sanity/svelte-loader

  • hydrogen - Shopify Hydrogen with Sanity

  • project-structure - Standalone Studio and monorepo patterns

  • app-sdk - Custom applications with Sanity App SDK

  • dashboard - Studios and apps in the Sanity Dashboard: defineApplication, local dev, deploys, Dashboard hooks

  • blueprints - Infrastructure as Code: blueprint files, stacks, plan/deploy workflow, error recovery, CI deploys

  • functions - Automating content workflows with Sanity Functions and webhooks

Topic Guides
  • groq - GROQ query patterns, type safety, performance optimization

  • schema - Schema design, field definitions, validation, deprecation patterns

  • visual-editing - Presentation Tool, Stega, overlays, live preview

  • page-builder - Page Builder arrays, block components, live editing

  • portable-text - Rich text rendering and custom components

  • image - Image schema, URL builder, hotspots, LQIP, Next.js Image

  • studio-structure - Desk structure, singletons, navigation

  • typegen - TypeGen configuration, workflow, type utilities

  • seo - Metadata, sitemaps, Open Graph, JSON-LD

  • localization - i18n patterns, document vs field-level, locale management

  • migration - Content import overview (see also migration-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.md

Each reference file contains:

  • Comprehensive topic or integration coverage

  • Incorrect and correct code examples

  • Decision matrices and workflow guidance

  • Framework-specific patterns where applicable

Files

references/app-sdk.md


title: Sanity App SDK description: Rules for building custom applications with the Sanity App SDK, including React hooks, document handles, real-time patterns, and Suspense best practices.

Sanity App SDK

Build custom React applications that interact with Sanity content in real-time.

If sanity.cli.ts uses defineApplication, also load the dashboard rule. It covers config, local development, deploys, and Dashboard hooks for apps in the Sanity Dashboard beta, and takes precedence over the Configuration and Commands sections below.

Tech Stack

  • Framework: React 19+, TypeScript

  • Packages: @sanity/sdk, @sanity/sdk-react v3 (v2 cannot sign in through the Dashboard)

  • Optional UI: @sanity/ui v4, styled-components

  • Runtime: Node.js 22.12+ (required by the current Sanity CLI)

Commands

# Basic quickstart (add --dashboard for the Dashboard beta, see the `dashboard` rule)
npx sanity@latest init --template app-quickstart --organization <your-org-id> --output-path . --typescript --skip-mcp

# With Sanity UI components
npx sanity@latest init --template app-sanity-ui --organization <your-org-id> --output-path . --typescript --skip-mcp

# Start development server
npm run dev

# Deploy to Sanity
npx sanity@latest deploy

# Install Sanity UI
npm install @sanity/ui@^4 styled-components

Project Structure

my-app/
├── sanity.cli.ts        # CLI config (org ID, entry point)
├── src/
│   ├── App.tsx          # Root component with SanityApp provider
│   ├── App.css          # Global styles
│   └── components/      # Your components
├── package.json
└── tsconfig.json

Boundaries

  • Always: Wrap data-fetching components in <Suspense>, use documentId as React key, read/write directly to Content Lake (not local state)

  • Always: Use useDocuments for lists, useDocumentProjection for display, useDocument + useEditDocument for editing

  • Ask first: Before using useQuery with raw GROQ (prefer useDocuments + useDocumentProjection)

  • Ask first: Before adding multiple data-fetching hooks in a single component

  • Never: Use useState for form values that should sync with Content Lake

  • Never: Use array index as React key for document lists (breaks real-time updates)

  • Never: Forget the fallback prop on <SanityApp> and <Suspense> boundaries

  • Never: Import Dashboard hooks (useNavigate, useNavigateToStudioDocument, useOrganizationId) from @sanity/sdk-react; they live in @sanity/sdk-react/dashboard


Configuration

CLI Config (sanity.cli.ts)

The classic app config, without defineApplication. For Dashboard beta apps and for app.visibility, see the dashboard rule.

import { defineCliConfig } from 'sanity/cli'

export default defineCliConfig({
  app: {
    organizationId: 'your-org-id',
    entry: './src/App.tsx',
  },
})
App Root (src/App.tsx)
import { SanityApp, type SanityConfig } from '@sanity/sdk-react'

export default function App() {
  const config: SanityConfig[] = [
    {
      projectId: 'your-project-id',
      dataset: 'production',
    },
  ]

  return (
    <SanityApp config={config} fallback={<div>Loading...</div>}>
      <YourComponents />
    </SanityApp>
  )
}
With Sanity UI

Sanity UI v4 needs its stylesheet imported once, and ToastProvider comes from @sanity/ui/toast:

import '@sanity/ui/styles.css'
import { SanityApp, type SanityConfig } from '@sanity/sdk-react'
import { ThemeProvider } from '@sanity/ui'
import { buildTheme } from '@sanity/ui/theme'
import { ToastProvider } from '@sanity/ui/toast'

const theme = buildTheme()

export default function App() {
  const config: SanityConfig[] = [
    { projectId: 'your-project-id', dataset: 'production' },
  ]

  return (
    <ThemeProvider theme={theme}>
      <ToastProvider>
        <SanityApp config={config} fallback={<div>Loading...</div>}>
          <YourComponents />
        </SanityApp>
      </ToastProvider>
    </ThemeProvider>
  )
}
Environment Variables

Prefix with SANITY_APP_ for automatic bundling:

SANITY_APP_PROJECT_ID=abc123
SANITY_APP_DATASET=production

Access: process.env.SANITY_APP_PROJECT_ID


Document Handles

Lightweight references to documents. Fetch handles first, then load content as needed.

interface DocumentHandle {
  documentId: string
  documentType: string
  projectId?: string
  dataset?: string
}
Creating Handles
// Best: From useDocuments hook
const { data: handles } = useDocuments({ documentType: 'article' })

// Good: With helper (preserves literal types for TypeGen)
import { createDocumentHandle } from '@sanity/sdk'
const handle = createDocumentHandle({
  documentId: 'my-doc-id',
  documentType: 'article',
})

// Good: With as const (preserves literal types)
const handle = {
  documentId: 'my-doc-id',
  documentType: 'article',
} as const

Hook Selection

HookUse CaseReturns
useDocumentsList of documents (infinite scroll)Document handles
usePaginatedDocumentsPaginated lists with page controlsDocument handles
useDocumentSingle document, real-time editingFull document or field
useDocumentProjectionSpecific fields, display onlyProjected data
useQueryComplex GROQ queries (use sparingly)Raw query results

Code Patterns

Fetching a Document List
// Good: Fetch handles, render items with Suspense
import { Suspense } from 'react'
import { useDocuments } from '@sanity/sdk-react'

function ArticleList() {
  const { data, hasMore, loadMore, isPending } = useDocuments({
    documentType: 'article',
    batchSize: 10,
    orderings: [{ field: '_updatedAt', direction: 'desc' }],
  })

  return (
    <>
      <ul>
        {data.map((handle) => (
          <Suspense key={handle.documentId} fallback={<li>Loading...</li>}>
            <ArticleItem {...handle} />
          </Suspense>
        ))}
      </ul>
      {hasMore && (
        <button onClick={loadMore} disabled={isPending}>
          Load More
        </button>
      )}
    </>
  )
}
// Bad: Over-fetching with raw GROQ, no pagination
function BadArticleList() {
  const { data } = useQuery(`*[_type == "article"]`)
  return data?.map((doc, i) => <li key={i}>{doc.title}</li>)
}
Projecting Content from a Handle
// Good: Project only needed fields
import { useDocumentProjection, type DocumentHandle } from '@sanity/sdk-react'

function ArticleItem(handle: DocumentHandle) {
  const { data } = useDocumentProjection({
    ...handle,
    projection: `{
      title,
      "authorName": author->name,
      "imageUrl": image.asset->url
    }`,
  })

  if (!data) return null

  return (
    <li>
      <h2>{data.title}</h2>
      <p>By {data.authorName}</p>
    </li>
  )
}
Real-time Editing
// Good: Read and write directly to Content Lake
import { useDocument, useEditDocument, type DocumentHandle } from '@sanity/sdk-react'

function TitleInput(handle: DocumentHandle) {
  const { data: title } = useDocument({ ...handle, path: 'title' })
  const editTitle = useEditDocument({ ...handle, path: 'title' })

  return (
    <input
      type="text"
      value={title ?? ''}
      onChange={(e) => editTitle(e.currentTarget.value)}
    />
  )
}
// Bad: Local state with submit button - causes stale data
function BadTitleForm(handle: DocumentHandle) {
  const [value, setValue] = useState('')
  const editTitle = useEditDocument({ ...handle, path: 'title' })

  function handleSubmit(e: FormEvent) {
    e.preventDefault()
    editTitle(value) // Only writes on submit!
  }

  return (
    <form onSubmit={handleSubmit}>
      <input value={value} onChange={(e) => setValue(e.target.value)} />
      <button type="submit">Save</button>
    </form>
  )
}
Document Actions
import {
  useApplyDocumentActions,
  publishDocument,
  unpublishDocument,
  deleteDocument,
} from '@sanity/sdk-react'

function DocumentActions({ handle }: { handle: DocumentHandle }) {
  const apply = useApplyDocumentActions()

  return (
    <div>
      <button onClick={() => apply(publishDocument(handle))}>Publish</button>
      <button onClick={() => apply(unpublishDocument(handle))}>Unpublish</button>
      <button onClick={() => apply(deleteDocument(handle))}>Delete</button>
    </div>
  )
}

Suspense Patterns

The App SDK uses React Suspense. Every data-fetching component must be wrapped.

Inside Studio form components: When App SDK hooks (or React.lazy, use()) run in a custom components.input, components.field, components.item, or a plugin's form.components, the boundary placement, fallback sizing, and new-document gating have Studio-specific rules. See the sanity-form-component-suspense skill.

One Hook Per Component
// Good: Separate fetchers into separate components
function EventsAndVenues() {
  return (
    <>
      <Suspense fallback="Loading events...">
        <EventsList />
      </Suspense>
      <Suspense fallback="Loading venues...">
        <VenuesList />
      </Suspense>
    </>
  )
}

function EventsList() {
  const { data } = useDocuments({ documentType: 'event' })
  return <List items={data} />
}

function VenuesList() {
  const { data } = useDocuments({ documentType: 'venue' })
  return <List items={data} />
}
// Bad: Multiple fetchers in one component
function BadComponent() {
  const { data: events } = useDocuments({ documentType: 'event' })
  const { data: venues } = useDocuments({ documentType: 'venue' })
  // Both trigger Suspense together, causing unnecessary re-renders
}
Prevent Layout Shift
// Good: Fallback matches final component dimensions
import { useNavigateToStudioDocument } from '@sanity/sdk-react/dashboard'

const BUTTON_TEXT = 'Open in Studio'

export function OpenInStudio({ handle }: { handle: DocumentHandle }) {
  return (
    <Suspense fallback={<Button text={BUTTON_TEXT} disabled />}>
      <OpenInStudioButton handle={handle} />
    </Suspense>
  )
}

function OpenInStudioButton({ handle }: { handle: DocumentHandle }) {
  const { navigateToStudioDocument } = useNavigateToStudioDocument(handle)
  return <Button onClick={navigateToStudioDocument} text={BUTTON_TEXT} />
}

Event Handling

import { useDocumentEvent, DocumentEvent } from '@sanity/sdk-react'

function DocumentWatcher(handle: DocumentHandle) {
  useDocumentEvent({
    ...handle,
    onEvent: (event) => {
      switch (event.type) {
        case 'edited':
          console.log('Edited:', event.documentId)
          break
        case 'published':
          console.log('Published:', event.documentId)
          break
        case 'deleted':
          console.log('Deleted:', event.documentId)
          break
      }
    },
  })

  return null
}

Multi-Project Apps

const config: SanityConfig[] = [
  { projectId: 'project-1', dataset: 'production' },
  { projectId: 'project-2', dataset: 'staging' },
]

// Handles include project/dataset info
const handle: DocumentHandle = {
  documentId: 'doc-123',
  documentType: 'article',
  projectId: 'project-1',
  dataset: 'production',
}

Lazy Loading with Refs

function LazyContent(handle: DocumentHandle) {
  const ref = useRef(null)

  const { data } = useDocumentProjection({
    ...handle,
    ref, // Only loads when element enters viewport
    projection: '{ title, body }',
  })

  return <div ref={ref}>{data?.title}</div>
}

What's NOT Included

The App SDK provides hooks and data stores. You bring:

  • UI components (use Sanity UI or your own)

  • Router

  • Form validation

  • Schema validation


Troubleshooting

IssueSolution
Safari dev issuesUse Chrome or Firefox during development
Port 3333 in usenpm run dev -- --port 3335 (a Dashboard app also takes the next port for the app itself)
Auth errorsnpx sanity@latest logout && npx sanity@latest login

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