Most CMS platforms force developers into a rigid content model and a WYSIWYG editor that produces unpredictable HTML. Sanity takes the opposite approach. The content is structured, stored as JSON, and queried through GROQ, a purpose-built query language. The editing interface is a React application you own and customize. The content lake is an API-first backend that works with any frontend framework, from Next.js to Astro to mobile apps.
Schema-driven content modeling
Content types in Sanity are defined in TypeScript as schema objects. There’s no admin panel for clicking fields together. The schema lives in the codebase, version-controlled alongside the application code.
import { defineType, defineField } from 'sanity';
export const project = defineType({
name: 'project',
title: 'Project',
type: 'document',
fields: [
defineField({
name: 'title',
type: 'string',
validation: (rule) => rule.required().max(100),
}),
defineField({
name: 'slug',
type: 'slug',
options: { source: 'title', maxLength: 96 },
}),
defineField({
name: 'description',
type: 'text',
rows: 3,
}),
defineField({
name: 'technologies',
type: 'array',
of: [{ type: 'string' }],
}),
defineField({
name: 'coverImage',
type: 'image',
options: { hotspot: true },
}),
],
});
defineType and defineField provide full TypeScript autocompletion. Validation rules are declared inline. The hotspot option on images lets editors define a focal point that responsive crops respect automatically. Every field maps to a JSON structure, so the content is portable and framework-agnostic.
GROQ: querying content without GraphQL overhead
GROQ is Sanity’s query language. It reads like a filter pipeline and returns exactly the shape you ask for. No over-fetching, no resolver functions, no schema stitching.
import { createClient } from '@sanity/client';
const client = createClient({
projectId: process.env.SANITY_PROJECT_ID!,
dataset: 'production',
apiVersion: '2024-09-01',
useCdn: true,
});
export async function getProjects() {
return client.fetch(`
*[_type == "project"] | order(_createdAt desc) {
title,
"slug": slug.current,
description,
technologies,
"imageUrl": coverImage.asset->url
}
`);
}
The query filters documents by type, orders them, and projects only the fields needed. The -> operator dereferences linked documents inline. useCdn: true serves cached responses from a global CDN, making reads fast enough for static generation and ISR in Next.js applications.
Customizing the Studio
The Sanity Studio is a React application. Every part of it, from the document list to the form inputs to the preview panes, is customizable through React components and plugins.
import { defineConfig } from 'sanity';
import { structureTool } from 'sanity/structure';
import { visionTool } from '@sanity/vision';
import { schemaTypes } from './schemas';
export default defineConfig({
name: 'portfolio',
title: 'Portfolio Studio',
projectId: process.env.SANITY_PROJECT_ID!,
dataset: 'production',
plugins: [structureTool(), visionTool()],
schema: { types: schemaTypes },
});
The visionTool plugin adds a GROQ playground directly in the studio for testing queries against live data. structureTool handles the document editing interface. Custom input components, preview cards, and document actions are all regular React components that plug into the configuration.
Portable Text for rich content
Instead of storing rich content as HTML, Sanity uses Portable Text, a JSON-based format that separates content from presentation. This means the same content renders differently on a website, a mobile app, and an email without any HTML parsing.
import { PortableText } from '@portabletext/react';
const components = {
types: {
image: ({ value }: { value: SanityImage }) => (
<figure>
<img src={urlFor(value).width(800).url()} alt={value.alt} />
{value.caption ? <figcaption>{value.caption}</figcaption> : null}
</figure>
),
code: ({ value }: { value: { code: string; language: string } }) => (
<pre data-language={value.language}>
<code>{value.code}</code>
</pre>
),
},
};
export function RichText({ content }: { content: PortableTextBlock[] }) {
return <PortableText value={content} components={components} />;
}
Custom block types like code snippets, embedded videos, or callouts are defined in the schema and rendered through the component map. The content stays structured and the rendering logic stays in the frontend where it belongs.
Conclusion
Sanity treats content as data, not markup. Schema-driven modeling keeps the content structure predictable, GROQ queries return exactly what the frontend needs, and the React-based Studio is as customizable as any component library. For developers building with Next.js or any modern framework, it’s a CMS that fits into the development workflow instead of working against it.