Sanity CMS: The Developer's Content Platform
Everything you need to know about Sanity — the headless CMS that treats content as structured data. Setup, GROQ queries, schemas, and real-time collaboration.
Sanity CMS: The Developer's Content Platform

If you've been building with WordPress or Contentful and hit a wall, Sanity might be exactly what you need. It's a headless CMS — but calling it just that undersells it. As of 2025, Sanity positions itself as a Content Operating System.
Let's break down what that means and why developers love it.
What is a Headless CMS?
Traditional CMS (WordPress, Joomla) = content + presentation bundled together.
Headless CMS = content stored as structured data, delivered via APIs. No frontend opinions. You build the frontend however you want — React, Next.js, Svelte, mobile apps, anything.
Traditional CMS:┌──────────────────────┐│ Content + Templates │ → One website└──────────────────────┘
Headless CMS:┌───────────┐ API ┌─── Website (Next.js)│ Content │ ────────→├─── Mobile App (React Native)│ (JSON) │ ├─── Digital Signage└───────────┘ └─── Voice AssistantYour content lives independently. Write once, publish everywhere.
Why Sanity?
There are many headless CMS options — Contentful, Strapi, Prismic. Here's what makes Sanity different:
1. Content Lake — Your content is stored as structured JSON in Sanity's managed backend. Real-time sync, versioning, and instant updates out of the box.
2. Sanity Studio — An open-source React app you fully customize. Define your own editing experience — not a generic admin panel.
3. GROQ — Sanity's own query language. More powerful than REST, more flexible than GraphQL for content queries.
4. Real-time Collaboration — Multiple editors see changes live, like Google Docs.
5. Portable Text — Rich text stored as structured data, not HTML blobs. You decide how to render it.
Setting Up Sanity
Getting started takes about 5 minutes:
# Install the CLInpm create sanity@latest
# Follow the prompts:# - Create or select a project# - Choose a dataset (production)# - Pick a template (blog, clean, etc.)
# Start the studiocd my-sanity-studionpm run devYour studio runs at localhost:3333 — a fully customizable editing environment.
Defining Schemas
Schemas are defined in code (TypeScript/JavaScript). This is where Sanity shines — your content model is version-controlled, reviewable, and flexible.
// schemas/post.tsimport { defineType, defineField } from 'sanity'
export default 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' }, }), defineField({ name: 'author', title: 'Author', type: 'reference', to: [{ type: 'author' }], }), defineField({ name: 'coverImage', title: 'Cover Image', type: 'image', options: { hotspot: true }, }), defineField({ name: 'body', title: 'Body', type: 'array', of: [ { type: 'block' }, // Rich text { type: 'image' }, // Inline images { type: 'code' }, // Code blocks ], }), defineField({ name: 'publishedAt', title: 'Published At', type: 'datetime', }), ],})Everything is typed, validated, and developer-friendly.
Querying with GROQ
GROQ (Graph-Relational Object Queries) is Sanity's query language. It's surprisingly intuitive:
// Fetch all published postsconst query = `*[_type == "post" && publishedAt < now()] | order(publishedAt desc) { title, "slug": slug.current, publishedAt, "authorName": author->name, "coverUrl": coverImage.asset->url}`
// Fetch a single post by slugconst postQuery = `*[_type == "post" && slug.current == $slug][0] { title, body, publishedAt, "author": author-> { name, "avatar": image.asset->url }, "related": *[_type == "post" && _id != ^._id] | order(publishedAt desc) [0...3] { title, "slug": slug.current }}`The -> dereferences references (like SQL JOINs but cleaner). The ^ refers to the parent document. You get exactly the shape of data you need — no over-fetching.
Using Sanity with Next.js
The most common combo. Here's a minimal setup:
// lib/sanity.tsimport { createClient } from '@sanity/client'
export const client = createClient({ projectId: 'your-project-id', dataset: 'production', apiVersion: '2026-02-24', useCdn: true, // false for real-time previews})
// Fetch postsexport async function getPosts() { return client.fetch( `*[_type == "post"] | order(publishedAt desc) { title, "slug": slug.current, publishedAt, "coverUrl": coverImage.asset->url }` )}
// Fetch single postexport async function getPost(slug: string) { return client.fetch( `*[_type == "post" && slug.current == $slug][0] { title, body, "author": author->{ name }, publishedAt }`, { slug } )}// app/blog/page.tsx (Next.js App Router)import { getPosts } from '@/lib/sanity'
export default async function BlogPage() { const posts = await getPosts()
return ( <div className="grid gap-6"> {posts.map((post) => ( <article key={post.slug}> <img src={post.coverUrl} alt={post.title} /> <h2>{post.title}</h2> <time>{new Date(post.publishedAt).toLocaleDateString()}</time> </article> ))} </div> )}Portable Text Rendering
Sanity stores rich text as Portable Text — structured JSON instead of HTML. You control exactly how each block renders:
// components/PortableText.tsximport { PortableText } from '@portabletext/react'
const components = { types: { image: ({ value }) => ( <img src={urlFor(value).width(800).url()} alt={value.alt} /> ), code: ({ value }) => ( <pre className="bg-gray-900 p-4 rounded-lg"> <code>{value.code}</code> </pre> ), }, marks: { link: ({ children, value }) => ( <a href={value.href} target="_blank" rel="noopener"> {children} </a> ), },}
export function PostBody({ body }) { return <PortableText value={body} components={components} />}No dangerouslySetInnerHTML. No HTML sanitization worries. Pure React components.
Sanity vs The Competition
| Feature | Sanity | Contentful | Strapi |
|---|---|---|---|
| **Hosting** | Managed | Managed | Self-hosted |
| **Studio** | Fully customizable (React) | Fixed UI | Admin panel |
| **Query Language** | GROQ + GraphQL | GraphQL + REST | REST + GraphQL |
| **Real-time** | Built-in | Webhooks only | Webhooks |
| **Rich Text** | Portable Text (JSON) | Rich Text (JSON) | Markdown/HTML |
| **Pricing** | Generous free tier | Expensive at scale | Free (self-host) |
| **TypeScript** | First-class | Good | Good |
What's New in 2025-2026
Sanity's Spring 2025 Release was massive:
- Canvas — AI-assisted writing environment (formerly Sanity Create)
- Media Library — Centralized asset management with versioning
- App SDK — Build custom apps inside Sanity
- Functions — Serverless functions for backend logic
- Agent Actions — AI that audits content, finds gaps, suggests updates
- Insights — Content performance analytics
It's no longer "just" a CMS — it's a full content operations platform.
When to Use Sanity
Great for:
- Multi-platform content (web + mobile + IoT)
- Teams that need real-time collaboration
- Developers who want full control over the editing experience
- Projects with complex content models
- Next.js / React projects
Maybe not for:
- Simple personal blogs (overkill — use Markdown)
- Non-technical teams without developer support
- Projects that need a built-in frontend (use WordPress instead)
- Try it: sanity.io/get-started
Getting Started
npm create sanity@latest has blog, e-commerce, and portfolio startersSanity is one of those tools that feels right once you "get" it. The learning curve is real (especially GROQ), but the payoff is a content system that actually works the way developers think.
Stay Updated 📬
Get the latest tips and tutorials delivered to your inbox. No spam, unsubscribe anytime.