Sanity Patterns
Quick Guide: Use Sanity for structured content management with GROQ queries, typed schemas viadefineType/defineField, and@sanity/clientfor data fetching. Always setapiVersionto a dated string, useuseCdn: truefor public reads, handle draft documents explicitly, use@sanity/image-urlfor image transformations, and render rich text with@portabletext/react. Generate TypeScript types withsanity typegen generate.
<critical_requirements>
CRITICAL: Before Using This Skill
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering, import type, named constants)
(You MUST always set apiVersion on createClient to a dated string like '2025-02-19' — omitting it uses a legacy API that may break)
(You MUST use useCdn: true for public read queries and useCdn: false when using a token or needing fresh data)
(You MUST use parameterized GROQ queries ($param) for any dynamic values — never interpolate user input into GROQ strings)
(You MUST handle drafts explicitly — draft documents have _id prefixed with drafts. and are not returned by default with perspective: 'published')
(You MUST use defineQuery() from groq and assign queries to named variables for TypeGen type generation)
</critical_requirements>
Auto-detection: Sanity, sanity, @sanity/client, createClient, GROQ, groq, defineType, defineField, defineArrayMember, @sanity/image-url, urlFor, @portabletext/react, PortableText, portable text, block content, sanity.config, sanity.cli, typegen, sanity studio, content lake
When to use:
- Setting up
@sanity/clientwithcreateClientfor data fetching - Writing GROQ queries (filters, projections, joins, ordering, slicing)
- Defining content schemas with
defineType,defineField,defineArrayMember - Rendering Portable Text (block content) with
@portabletext/react - Generating image URLs with
@sanity/image-url(responsive images, crops, hotspots) - Performing mutations (create, patch, delete, transactions)
- Setting up real-time listeners with
client.listen() - Generating TypeScript types with Sanity TypeGen
Key patterns covered:
- Client setup with
createClientandapiVersionconfiguration - GROQ query language: filters, projections, ordering, slicing, joins, references
- Schema definitions: document types, object types, arrays, references, images
- Portable Text rendering with custom components
- Image URL builder with responsive images and transformations
- Mutations: create, createOrReplace, patch, delete, transactions
- Real-time listeners via
client.listen() - TypeGen for type-safe GROQ queries with
defineQuery()
When NOT to use:
- GraphQL-only APIs (Sanity supports GROQ primarily; use GraphQL skill if needed)
- Direct database access (Sanity is a hosted content lake, not a database)
- Non-Sanity CMS platforms (use the dedicated skill for your CMS)
Detailed Resources:
- For decision frameworks and quick-reference tables, see [reference.md](reference.md)
Client & GROQ:
- [examples/core.md](examples/core.md) — Client setup, GROQ queries, error handling, TypeGen
Schemas:
- [examples/schemas.md](examples/schemas.md) — defineType, defineField, document types, object types, references, images
Rich Content:
- [examples/rich-content.md](examples/rich-content.md) — Portable Text rendering, image URL builder, responsive images
Mutations & Real-time:
- [examples/mutations.md](examples/mutations.md) — Create, patch, delete, transactions, real-time listeners
<philosophy>
Philosophy
Sanity is a structured content platform built around a real-time content lake, GROQ (Graph-Relational Object Queries) as its query language, and Sanity Studio as a customizable editing environment.
Core principles:
- Structured content — Content is defined by schemas (
defineType,defineField) that describe shape, validation, and editorial UI. Schemas are code, not configuration files. - GROQ-first querying — GROQ lets you filter, project, join, and reshape data in a single query. Unlike REST or GraphQL, GROQ queries return exactly the shape you define in the projection.
- Content as data — Rich text is stored as Portable Text (a JSON-based specification), making it renderable in any frontend framework without vendor lock-in.
- API versioning — Every client must specify an
apiVersiondate string. This pins your code to a specific API behavior, preventing breaking changes from affecting production. - CDN caching — Public read queries use
useCdn: truefor edge-cached responses. Mutations and authenticated reads useuseCdn: falsefor fresh data. - Type generation — Sanity TypeGen generates TypeScript types from both your schemas and GROQ queries, enabling end-to-end type safety from content model to frontend.
When to use Sanity:
- Content-driven websites and applications (blogs, marketing sites, documentation)
- Projects needing real-time collaborative editing in a customizable studio
- Multi-channel content delivery (web, mobile, IoT) from a single content source
- Teams wanting type-safe content queries with GROQ and TypeGen
When NOT to use:
- Transactional data requiring ACID guarantees (use a database)
- User-generated content at massive scale (Sanity is optimized for editorial content)
- Projects needing a self-hosted CMS (Sanity's content lake is hosted, though the Studio is open source)
</philosophy>
<patterns>
Core Patterns
Pattern 1: Client Setup with createClient
Configure @sanity/client with project ID, dataset, API version, and CDN preference. Always set apiVersion to a dated string and useCdn explicitly.
import { createClient } from "@sanity/client";
export const client = createClient({
projectId: process.env.SANITY_PROJECT_ID!,
dataset: process.env.SANITY_DATASET!,
apiVersion: "2025-02-19", // Pin to a specific API version date
useCdn: true, // true for public reads, false for authenticated/fresh data
});
For dual client setup (public + preview with token), see [examples/core.md](examples/core.md).
Pattern 2: GROQ Queries with Filters and Projections
GROQ queries combine filters, projections, ordering, and slicing. Always use defineQuery() for TypeGen and $param for dynamic values.
import { defineQuery } from "groq";
const POST_BY_SLUG_QUERY = defineQuery(`
*[_type == "post" && slug.current == $slug][0]{
_id, title, body, "author": author->{name, image}
}
`);
const post = await client.fetch(POST_BY_SLUG_QUERY, { slug: "my-post" });
Never interpolate user input into GROQ strings -- always use $param parameters to prevent GROQ injection. For advanced queries (combined queries, conditional projections), see [examples/core.md](examples/core.md).
Pattern 3: Schema Definitions with defineType and defineField
Define content structure with defineType, defineField, and defineArrayMember from "sanity" for type safety and Studio UI.
import { defineType, defineField } from "sanity";
export const postType = defineType({
name: "post",
type: "document",
fields: [
defineField({
name: "title",
type: "string",
validation: (r) => r.required(),
}),
defineField({ name: "slug", type: "slug", options: { source: "title" } }),
],
});
For complete schemas (images with hotspot, arrays, references, previews, object types), see [examples/schemas.md](examples/schemas.md).
Pattern 4: Portable Text Rendering
Render block content with @portabletext/react. Define custom PortableTextComponents for non-standard blocks (images, code) and marks (links, highlights).
import { PortableText } from "@portabletext/react";
<PortableText value={body} components={components} />;
Do not use the deprecated @sanity/block-content-to-react pac