Why Sanity for Modern Development
Sanity has established itself as one of the most developer-friendly headless CMS platforms available today. Unlike traditional content management systems that couple content with presentation, Sanity provides a fully customizable, API-first approach to content management that empowers developers to build exactly what they need while giving content editors a powerful, real-time editing experience.
Headless content management systems represent a fundamental shift in how we think about digital content. Traditional CMS platforms like WordPress tightly couple the content repository with the presentation layer, meaning your content lives in HTML templates and database tables designed for web pages. Sanity takes a different approach: content is stored as structured data in a format-agnostic Content Lake, accessible only through powerful APIs that deliver JSON to any frontend, device, or channel.
This architectural decision enables remarkable flexibility. Your content can flow to a Next.js website, a mobile application, a smart watch interface, or even an IoT device--all from a single source of truth. Content editors work in a dedicated interface optimized for their needs, while developers build frontends using modern frameworks without being constrained by CMS template limitations. The separation also means your content model can evolve independently of your presentation layer, making it easier to redesign websites or add new channels without restructuring your content.
Whether you're building a marketing website, a complex web application, or a multi-channel content platform, Sanity's flexible architecture adapts to your needs. The platform's commitment to developer experience--TypeScript support, real-time collaboration, and a powerful query language--makes it our default recommendation for modern content-driven projects. Our web development services frequently integrate Sanity to provide clients with a content management experience that matches their technical capabilities and business requirements.
For organizations focused on search visibility, pairing Sanity with our SEO services creates a powerful combination. Structured content from a headless CMS like Sanity gives search engines clean, semantic data that improves indexing and rankings. The API-first architecture means your content is optimized for discovery from the ground up.
Key differentiators that make Sanity our preferred headless CMS choice
Fully Customizable Studio
Sanity Studio is a React application you can modify to match your content team's workflow exactly.
Real-Time Collaboration
Multiple team members can edit simultaneously with live updates and instant previews.
GROQ Query Language
A powerful query language designed specifically for content applications, with flexible projections and joins.
Generous Free Tier
100K API requests and 10GB bandwidth per month on the free plan, with affordable scaling.
Setting Up Your First Sanity Project
Prerequisites and Account Creation
Before creating your first Sanity project, ensure you have Node.js version 18 or later installed, as Sanity's tooling requires a modern Node environment. You'll also need a terminal application--PowerShell, Command Prompt, or any Unix shell works equally well. While you can work with Sanity entirely through the web interface, the command-line tools provide the most efficient development experience.
Begin by creating a Sanity account at accounts.sanity.io. The signup process asks for minimal information--just an email address and password--and you can authenticate with Google or GitHub if preferred. Once registered, you'll have access to the Sanity Dashboard, which displays all your projects and provides quick access to management features. However, most of your interaction with Sanity will flow through the CLI, which handles authentication through your browser when needed.
Installing the CLI and Creating Your Project
The Sanity CLI provides commands for everything from initial project creation to deployment and data management. Install it globally using your preferred package manager:
# Install CLI globally
npm install -g @sanity/cli
Or with pnpm:
pnpm add -g @sanity/cli
Once installed, authenticate by running:
sanity login
This opens your browser where you can sign in with your Sanity account, granting the CLI access to manage your projects. Authentication persists across sessions, so you won't need to log in repeatedly.
With authentication complete, create a new project:
npm create sanity@latest
The CLI prompts you through a guided setup process. First, choose between creating a new project or selecting an existing one--select "Create new project." Enter a descriptive name for your project; this appears in your dashboard and helps identify projects when you have multiple. The CLI then asks for a project output path, specifying where on your local machine the project files should reside.
Choosing a Template
The most significant decision during setup is selecting a project template. Sanity offers several options, each suited to different use cases. The "Clean project" template provides a minimal setup with no predefined schemas--an ideal starting point when you want complete control over your content model. This approach gives you a blank canvas, requiring you to define every document type and field, which ultimately results in a schema precisely tailored to your content needs.
Alternatively, you can choose from templates pre-configured for common use cases. The Blog template includes document types for posts, authors, and categories with appropriate fields and relationships already defined. The E-commerce template models products, variants, and categories with inventory and pricing fields. These templates serve as excellent starting points and learning examples--you can modify them extensively or use them as references when building your own schemas.
For learning purposes, we recommend the Clean project template. While starting from scratch requires more initial work, it ensures you understand the fundamental concepts of Sanity schema design rather than inheriting assumptions from template authors. The knowledge transfers directly to modifying any template later when you need to adapt it to specific requirements.
The CLI also asks whether to use TypeScript, and we strongly recommend yes. Sanity's TypeScript support is excellent, with full type inference for schema definitions, query results, and API responses. TypeScript catches errors at compile time, documents your content model through type annotations, and provides superior IDE integration for autocomplete and navigation.
Once the CLI finishes scaffolding, navigate to your project directory and start the development server:
cd your-project-name
npm run dev
The Studio becomes available at http://localhost:3333 by default. The development server watches for file changes and automatically reloads the Studio, so you can iterate on your schema while the Studio remains open.
For teams looking to automate content workflows, our AI automation services can integrate with Sanity's webhooks and APIs to create intelligent content pipelines that streamline publishing and distribution.
Understanding Sanity Schemas
The Schema System Architecture
Sanity's schema system defines your content model through JavaScript (or TypeScript) files that describe document types, their fields, and relationships. These schema files live in the schemas directory, typically organized into subdirectories for different concerns--documents for content types that appear in the structure view, objects for reusable component types, and possibly separate files for specific content areas.
Schema definitions use the defineType and defineField functions from the Sanity package. These functions provide type safety, enable IDE autocompletion, and ensure your schema follows expected patterns. While you could write plain objects, using the define functions provides significant benefits in development experience and catch errors early.
The schema system distinguishes between document types and object types. Documents are top-level content entities that appear in the Studio's structure view and can be created, edited, and deleted independently. Objects are reusable components that can only exist within documents--they might be referenced by multiple documents or appear within arrays, but they don't exist on their own. This distinction mirrors how content works: a blog post is a document, while a block of rich text or an image with caption is an object that appears within posts.
Creating Document Types
A document type definition specifies the content structure for a particular kind of content in your system. Let's examine a complete example for a blog post:
import { defineType, defineField } from 'sanity'
export const post = defineType({
name: 'post',
title: 'Blog Post',
type: 'document',
fields: [
defineField({
name: 'title',
title: 'Title',
type: 'string',
validation: (Rule) => Rule.required().max(100),
}),
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: 'body',
title: 'Body',
type: 'blockContent',
}),
],
})
This schema demonstrates several important concepts. The name field provides a programmatic identifier used throughout the API--when querying for posts, you'll filter by _type == "post". The title field provides a human-readable label that appears in the Studio interface. The type property designates this as a document type.
Field definitions use defineField for consistency and IDE support. Each field specifies its data type through the type property--string for text, slug for URL segments, reference for cross-document links, array for lists, image for media, and datetime for timestamps. The options object configures type-specific behavior, such as specifying which field should generate the slug value or enabling image hotspots for responsive cropping.
Validation rules ensure data quality before content publishes. The validation property accepts a function that receives a Rule object with methods for required fields, length limits, pattern matching, and cross-field validation. Sanity validates content automatically as editors work, providing immediate feedback about problems rather than waiting until submission.
The preview configuration controls how documents appear in list views and reference previews. The select object maps preview fields to document paths, and prepare receives the selected values for transformation. This flexibility lets you create meaningful previews that help editors navigate large content repositories.
Working with Object Types
Object types define reusable content components that appear within documents. They're essential for creating flexible content structures, particularly when building page builder systems. Let's create an object type for a "ZigZag" section--a common pattern with alternating text and image:
export const zigZag = defineType({
name: 'zigZag',
title: 'Zig-Zag Section',
type: 'object',
fields: [
defineField({ name: 'title', type: 'string' }),
defineField({
name: 'image',
type: 'image',
options: { hotspot: true },
fields: [
{ name: 'alt', type: 'string', title: 'Alternative Text' },
],
}),
defineField({
name: 'layout',
type: 'string',
options: { list: ['left', 'right'] },
}),
],
})
This object type can be embedded within documents through array fields. Unlike documents, objects don't appear in the structure view and can't be created independently. They exist as components that documents assemble into larger content structures. Objects are ideal for reusable patterns like testimonials, CTA sections, or any content component that appears in multiple contexts.
The ZigZag example shows several additional field types: text for longer strings with multiple rows, image with nested alt text field, and string with a list option for enumerations. The initialValue property sets a default, simplifying the editing experience for frequently-used patterns.
Registering Schemas
After creating schema files, register them by importing them in schemas/index.ts:
import { type SchemaTypeDefinition } from 'sanity'
import { post } from './documents/post'
import { author } from './documents/author'
import { category } from './documents/category'
import { zigZag } from './objects/zigZag'
import { blockContent } from './objects/blockContent'
export const schema: { types: SchemaTypeDefinition[] } = {
types: [post, author, category, zigZag, blockContent],
}
The types array includes all document and object types in your project. Sanity reads this array during startup, validating that all types are properly defined and building the internal schema representation used throughout the platform. This registration is automatic--any type you import and include will be available in the Studio immediately.
Proper schema organization is essential for maintainability. We recommend grouping related schemas in subdirectories (documents/, objects/, components/) and keeping the main index file clean and focused on exports. This structure scales well as projects grow, making it easy to locate and modify schema definitions.
For more advanced schema patterns and content modeling strategies, see our guide on Sanity Schema Design, which covers complex relationships, validation strategies, and scalable content architectures.
Customizing Sanity Studio
Structure Configuration
While Sanity's default structure view works well for many projects, customizing the structure improves the editing experience for complex content models. The structure configuration controls what appears in the navigation pane, how documents are grouped, and what default views appear when opening each document type.
Create a structure configuration at structure/index.ts:
import { type StructureResolver } from 'sanity/structure'
import { DocumentIcon, HomeIcon } from '@sanity/icons'
export const structure: StructureResolver = (S) =>
S.list()
.title('Content')
.items([
S.documentListItem()
.schemaType('homePage')
.title('Home')
.id('home')
.icon(HomeIcon),
S.documentTypeListItem('post')
.title('Posts')
.icon(DocumentIcon),
S.documentTypeListItem('author')
.title('Authors')
.icon(DocumentIcon),
S.documentTypeListItem('category')
.title('Categories')
.icon(DocumentIcon),
])
This structure creates a single "Content" panel with four items: a single document for the home page (not a list), and document type lists for posts, authors, and categories. The id parameter on the home page item ensures only one instance exists--we're editing a single page, not creating multiple.
The structure builder provides methods for lists, document views, form views, and custom components. You can create folders for organization, split types across sections, add custom preview components, and configure initial sorting and filtering. Complex structures might include conditional items, user role-based visibility, or integration with external data sources.
Apply the structure by passing it to the structure tool in your Sanity configuration:
import { defineConfig } from 'sanity'
import { structureTool } from 'sanity/structure'
import { structure } from './structure'
export default defineConfig({
name: 'default',
title: 'Your Project Name',
plugins: [structureTool({ structure })],
schema: { types: schemaTypes },
})
Adding Custom Input Components
Sanity allows replacing default input components with custom React components, enabling specialized editing experiences for complex content types. The plugin ecosystem provides ready-made components for common needs--you'll frequently use the Unsplash asset source for image search:
npm install sanity-plugin-asset-source-unsplash
Add it to your configuration:
import { defineConfig } from 'sanity'
import { unsplashImageAsset } from 'sanity-plugin-asset-source-unsplash'
export default defineConfig({
plugins: [unsplashImageAsset()],
// other plugins
})
After installation, image fields include an Unsplash option alongside the standard upload option, letting editors search and select professional photography without leaving the Studio.
The plugin system is one of Sanity's greatest strengths. Beyond Unsplash, you'll find plugins for icon management, color pickers, document reviews, editorial workflows, and much more. Most plugins install with a single npm command and configure with a few lines of code. Custom input components can be built for any purpose when you need specialized functionality beyond the standard field types, as documented in the official schema types documentation.
For deeper customization of the Studio interface, including custom desk structures and workflow automation, explore our comprehensive guide on Sanity Studio Customization.
Introducing GROQ Queries
Why GROQ Matters
GROQ (Graph-Oriented Query Language) is Sanity's answer to content querying, designed specifically for the patterns content applications require. While GraphQL has gained popularity for API design, GROQ offers several advantages for CMS use cases that make it our preferred choice.
GROQ's projection syntax lets you specify exactly which fields you need in a single, readable line. A query to fetch a post with its author, categories, and image data reads almost like the result structure you want. This readability reduces errors and makes queries easier to maintain, especially as content models grow complex.
The language handles references naturally, with traversal syntax that follows relationships without separate API calls or nested resolvers. When you request author.name, GROQ automatically fetches the referenced author document and extracts the name field. This denormalization at query time means efficient single-request fetches while maintaining normalized content storage.
GROQ also includes computed fields, array operations, and powerful filtering capabilities. You can transform data within queries, filter by nested conditions, order results, and limit counts--all without backend code modifications. This flexibility means frontend developers can adapt to new requirements without schema or API changes.
Basic GROQ Queries
Let's examine some fundamental GROQ queries for the blog schema we created:
// Fetch all published posts, ordered by date
export const postsQuery = `*[_type == "post" && defined(publishedAt)]
| order(publishedAt desc) {
_id, title, slug, publishedAt,
"author": author->name,
"categories": categories[]->title,
"excerpt": array::join(string::split(pt::text(body), "")[0..200], "...")
}`
This query demonstrates several GROQ concepts. *[_type == "post"] selects all documents of type "post," with an additional filter for published posts (those with a publishedAt date). The | order(publishedAt desc) pipe sorts results in descending order by date.
The projection in braces specifies which fields to include. The "author": author->name syntax traverses the reference to fetch the author's name, assigning it to an author field in the result. The "categories": categories[]->title syntax is similar but handles arrays, fetching the title from each referenced category.
The "excerpt" assignment shows computed fields in action. pt::text(body) extracts plain text from Portable Text content, string::split(..., "")[0..200] takes the first 200 characters, and array::join(..., "...") adds an ellipsis. This transformation happens at query time, giving the frontend a ready-made excerpt without processing logic.
// Fetch a single post by slug
export const postBySlugQuery = `*[_type == "post" && slug.current == $slug][0] {
_id, title, body,
"author": author->{name, image, bio}
}`
The [0] accessor returns only the first matching document (useful when filtering by unique fields like slug). The parameter $slug passes the slug from the API call, protecting against injection attacks.
The "author": author->{name, image, bio} projection fetches multiple fields from the referenced author document in a single query. This nested projection keeps queries flat while including related data--a pattern that significantly reduces frontend complexity.
Fetching Data in Your Application
The sanity/client package provides the API for running queries from your frontend. Configure a client instance:
import { createClient } from '@sanity/client'
import imageUrlBuilder from '@sanity/image-url'
export const client = createClient({
projectId: process.env.SANITY_PROJECT_ID,
dataset: 'production',
apiVersion: '2024-01-01',
useCdn: true,
})
const builder = imageUrlBuilder(client)
export function urlFor(source: any) {
return builder.image(source).width(600).height(400).url()
}
The apiVersion parameter is required--it pins your queries to a specific API version, ensuring queries continue working even as Sanity evolves. The useCdn option enables the content delivery network for faster response times in production, caching results at edge locations worldwide.
The imageUrlBuilder chain provides URL transformations for Sanity's image CDN. Methods like width(), height(), blur(), and format() generate URLs with appropriate transformations, letting you request exactly the image variant you need. The CDN handles resizing and optimization on-demand, reducing bandwidth and improving page load times for your AI-powered web applications.
import { client, postsQuery } from '@/lib/sanity'
export default async function BlogPage() {
const posts = await client.fetch(postsQuery)
return (
<div>
{posts.map((post) => (
<article key={post._id}>
<h2>{post.title}</h2>
<p>By {post.author}</p>
</article>
))}
</div>
)
}
As documented in the official GROQ documentation, queries can be defined as exported strings and imported wherever needed, promoting reusability and keeping query logic centralized.
To master GROQ beyond the basics, including advanced projections, joins, and performance optimization, continue with our dedicated guide on Sanity GROQ Queries.
Deploying Your Studio
Hosting Options
Sanity Studio can be deployed to various hosting platforms, with Sanity's own hosting service providing the simplest option for many projects. The Studio is a static Single Page Application after build, making it compatible with any static hosting service--Vercel, Netlify, Cloudflare Pages, or traditional CDN hosting.
Sanity's hosted service at sanity.studio provides zero-configuration deployment with automatic SSL, global CDN distribution, and seamless integration with your Sanity project. For most projects, this hosting option provides the best balance of simplicity and performance.
Deploying to Sanity Studio Hosting
Deploying to Sanity's hosted service requires a single command:
npm run deploy
On first deployment, the CLI prompts you to choose a subdomain for your Studio. This subdomain must be unique across all Sanity projects, so choose something distinctive. Once selected, the Studio deploys to https://your-subdomain.sanity.studio.
Subsequent deployments use the same command. The CLI detects unchanged files and deploys only what's necessary, making deployments fast even for larger projects. Changes deploy atomically--no downtime occurs during deployment, and users see the new version instantly once the deploy completes.
Deploying to External Platforms
For teams with existing hosting infrastructure or specific requirements, Sanity Studio can deploy to any static hosting service. The build command generates static files:
npm run build
The output can be deployed using your hosting provider's standard deployment process. Consult your hosting provider's documentation for deployment instructions--most support git-based deployments, CLI uploads, or CI/CD integration.
Deploying externally gives you more control over the deployment pipeline but requires additional configuration. You'll need to configure environment variables in your hosting platform, set up preview URLs for visual editing, and manage deployment monitoring separately from Sanity's infrastructure.
Environment Variables
Production deployments require environment variables for your project ID and dataset. These should never be committed to version control--use your hosting platform's environment variable configuration instead.
# .env.local (development)
NEXT_PUBLIC_SANITY_PROJECT_ID=your-project-id
NEXT_PUBLIC_SANITY_DATASET=production
For production, set these variables in your hosting platform's environment configuration. The specific process varies by provider--Vercel uses project settings, Netlify uses environment variables in the UI or netlify.toml, and other platforms have equivalent mechanisms. As emphasized in the Sanity CLI documentation, environment variable security is essential: never commit sensitive values to version control.
Different variables are needed for development versus production environments. The NEXT_PUBLIC_ prefix in Next.js applications exposes these values to the browser, which is necessary for client-side Sanity queries. Server-side queries can use regular environment variables without the prefix for additional security.
For production deployments with complex requirements, our web development team has extensive experience configuring Sanity with various hosting providers and CI/CD pipelines to ensure reliable, scalable content management infrastructure.
Sources
Sanity Schema Design
Advanced patterns for complex content models, validation strategies, and content relationships.
Learn moreSanity GROQ Queries
Master GROQ with advanced projections, joins, aggregations, and query optimization techniques.
Learn moreSanity + Next.js Integration
Build complete web applications with Sanity content in Next.js, including live preview.
Learn more