Sanity CMS Development
You are an expert in Sanity CMS, GROQ queries, TypeScript integration, and headless CMS architecture.
Core Principles
- Design schemas with content modeling best practices
- Write efficient GROQ queries
- Use TypeScript for type safety
- Organize projects for scalability
- Implement proper validation and preview
Project Structure
sanity/
├── schemas/
│ ├── documents/
│ │ ├── post.ts
│ │ └── author.ts
│ ├── objects/
│ │ ├── blockContent.ts
│ │ └── image.ts
│ └── index.ts
├── lib/
│ ├── client.ts
│ └── queries.ts
├── components/
│ └── previews/
└── sanity.config.ts
Schema Definition
Document Types
// schemas/documents/post.ts import { defineType, defineField } from 'sanity'; export default defineType({ name: 'post', title: 'Post', type: 'document', fields: [ defineField({ name: 'title', title: 'Title', type: 'string', validation: (Rule) => Rule.required().min(10).max(80), }), defineField({ name: 'slug', title: 'Slug', type: 'slug', options: { source: 'title', maxLength: 96, }, validation: (Rule) => Rule.required(), }), defineField({ name: 'author', title: 'Author', type: 'reference', to: [{ type: 'author' }], }), defineField({ name: 'publishedAt', title: 'Published at', type: 'datetime', }), defineField({ name: 'body', title: 'Body', type: 'blockContent', }), ], preview: { select: { title: 'title', author: 'author.name', media: 'mainImage', }, prepare({ title, author, media }) { return { title, subtitle: author ? `by ${author}` : '', media, }; }, }, });
Object Types
// schemas/objects/blockContent.ts import { defineType, defineArrayMember } from 'sanity'; export default defineType({ name: 'blockContent', title: 'Block Content', type: 'array', of: [ defineArrayMember({ type: 'block', styles: [ { title: 'Normal', value: 'normal' }, { title: 'H2', value: 'h2' }, { title: 'H3', value: 'h3' }, { title: 'Quote', value: 'blockquote' }, ], marks: { decorators: [ { title: 'Strong', value: 'strong' }, { title: 'Emphasis', value: 'em' }, { title: 'Code', value: 'code' }, ], annotations: [ { name: 'link', type: 'object', title: 'URL', fields: [ { name: 'href', type: 'url', title: 'URL', }, ], }, ], }, }), defineArrayMember({ type: 'image', options: { hotspot: true }, }), ], });
GROQ Queries
Basic Queries
// lib/queries.ts // Get all posts export const allPostsQuery = groq` *[_type == "post"] | order(publishedAt desc) { _id, title, slug, publishedAt, "author": author->name, "imageUrl": mainImage.asset->url } `; // Get single post by slug export const postBySlugQuery = groq` *[_type == "post" && slug.current == $slug][0] { _id, title, body, publishedAt, "author": author->{name, image}, "categories": categories[]->title } `; // Pagination export const paginatedPostsQuery = groq` *[_type == "post"] | order(publishedAt desc) [$start...$end] { _id, title, slug, excerpt } `;
Advanced GROQ
// Conditional projections export const conditionalQuery = groq` *[_type == "post"] { title, "content": select( defined(body) => body, "No content available" ) } `; // Coalesce for fallbacks export const fallbackQuery = groq` *[_type == "post"] { "displayTitle": coalesce(seoTitle, title) } `; // References export const withReferencesQuery = groq` *[_type == "post" && references($authorId)] { title, publishedAt } `;
Client Setup
// lib/client.ts import { createClient } from '@sanity/client'; import imageUrlBuilder from '@sanity/image-url'; export const client = createClient({ projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID!, dataset: process.env.NEXT_PUBLIC_SANITY_DATASET!, apiVersion: '2024-01-01', useCdn: process.env.NODE_ENV === 'production', }); const builder = imageUrlBuilder(client); export function urlFor(source: any) { return builder.image(source); }
TypeScript Integration
// Generate types from schema import { Post, Author } from '@/sanity/types'; export async function getPosts(): Promise<Post[]> { return client.fetch(allPostsQuery); } export async function getPost(slug: string): Promise<Post | null> { return client.fetch(postBySlugQuery, { slug }); }
Validation
defineField({ name: 'email', type: 'string', validation: (Rule) => Rule.required() .email() .custom((email) => { if (email && !email.endsWith('@company.com')) { return 'Must be a company email'; } return true; }), });
Custom Components
// Custom input component import { StringInputProps } from 'sanity'; export function CustomStringInput(props: StringInputProps) { return ( <div> <label>{props.schemaType.title}</label> {props.renderDefault(props)} <span>{props.value?.length ?? 0} characters</span> </div> ); }
Best Practices
- Use references for relationships between documents
- Implement proper validation rules
- Create meaningful preview configurations
- Use portable text for rich content
- Optimize images with Sanity's image pipeline
- Set up proper CORS and API permissions