ShaharAmir
← Back to Blog
JavaScript6 min read

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.

S
Shahar Amir

Sanity CMS: The Developer's Content Platform

Sanity Logo

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.

text
12345678910
Traditional CMS:
┌──────────────────────┐
│ Content + Templates │ → One website
└──────────────────────┘
Headless CMS:
┌───────────┐ API ┌─── Website (Next.js)
│ Content │ ────────→├─── Mobile App (React Native)
│ (JSON) │ ├─── Digital Signage
└───────────┘ └─── Voice Assistant

Your 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:

bash
1234567891011
# Install the CLI
npm create sanity@latest
# Follow the prompts:
# - Create or select a project
# - Choose a dataset (production)
# - Pick a template (blog, clean, etc.)
# Start the studio
cd my-sanity-studio
npm run dev

Your 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.

typescript
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849
// schemas/post.ts
import { 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:

javascript
1234567891011121314151617181920212223
// Fetch all published posts
const 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 slug
const 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:

typescript
12345678910111213141516171819202122232425262728293031323334
// lib/sanity.ts
import { 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 posts
export async function getPosts() {
return client.fetch(
`*[_type == "post"] | order(publishedAt desc) {
title,
"slug": slug.current,
publishedAt,
"coverUrl": coverImage.asset->url
}`
)
}
// Fetch single post
export async function getPost(slug: string) {
return client.fetch(
`*[_type == "post" && slug.current == $slug][0] {
title,
body,
"author": author->{ name },
publishedAt
}`,
{ slug }
)
}

typescript
123456789101112131415161718
// 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:

typescript
1234567891011121314151617181920212223242526
// components/PortableText.tsx
import { 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

FeatureSanityContentfulStrapi
**Hosting**ManagedManagedSelf-hosted
**Studio**Fully customizable (React)Fixed UIAdmin panel
**Query Language**GROQ + GraphQLGraphQL + RESTREST + GraphQL
**Real-time**Built-inWebhooks onlyWebhooks
**Rich Text**Portable Text (JSON)Rich Text (JSON)Markdown/HTML
**Pricing**Generous free tierExpensive at scaleFree (self-host)
**TypeScript**First-classGoodGood

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)
  • Getting Started

    1. Try it: sanity.io/get-started
  • Templates: npm create sanity@latest has blog, e-commerce, and portfolio starters
  • Docs: sanity.io/docs — some of the best CMS docs out there
  • Free tier: 200K API requests/month, 1M assets, 3 users — plenty for most projects
  • Sanity 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.

    #cms#sanity#headless-cms#next-js#react

    Stay Updated 📬

    Get the latest tips and tutorials delivered to your inbox. No spam, unsubscribe anytime.