Best CMS Solutions for Next.js in 2026
TL;DR
Sanity for developer-first flexibility; Contentful for enterprise; Payload for self-hosted TypeScript control. Sanity (~600K weekly downloads) is the developer's CMS — real-time collaboration, GROQ query language, and React-based studio. Contentful (~400K) is the established enterprise choice. Payload CMS (~200K, fast-growing) is TypeScript-native, self-hosted, and generates its own admin UI from your schema — zero vendor lock-in. Keystatic is the emerging file-based option that stores content in your Git repository.
Key Takeaways
- Sanity: ~600K weekly downloads — GROQ queries, real-time, React studio, free tier
- Contentful: ~400K downloads — enterprise feature set, established market leader
- Payload CMS: ~200K downloads — self-hosted, TypeScript-first, admin UI auto-generated
- Keystatic — Git-backed CMS, no database needed, content stored in your repo
- Hygraph (formerly GraphCMS) — GraphQL-native, strong federation
How to Choose a CMS for Next.js
The right headless CMS for a Next.js project depends on three factors more than anything else: who edits content, who owns infrastructure, and how complex your content model is.
If your editorial team needs a polished interface and real-time collaboration, Sanity or Contentful are the right choices — both have mature admin experiences built for non-technical editors. If you are a developer-only team building an internal tool or a startup MVP and you want to avoid vendor lock-in, Payload CMS gives you everything in your own codebase. If your content is mostly blog posts and documentation and you want zero runtime cost, Keystatic stores everything in Git and requires no database.
The Next.js App Router (introduced in Next.js 13 and stabilized in 14–15) changed how CMS data fetching works. Most modern CMS SDKs now have first-class support for React Server Components — you fetch data in an async server component without useEffect or client-side loading states. The examples below use this pattern throughout.
Package Health Table
| CMS | Weekly Downloads | Hosting | Free Tier | TypeScript |
|---|---|---|---|---|
@sanity/client | ~600K | Managed (Sanity Cloud) | 3 users, 5GB | Yes |
contentful | ~400K | Managed (Contentful Cloud) | 25K records | Yes (CLI) |
payload | ~200K | Self-hosted | Free | Yes, native |
@keystatic/core | ~50K | Git / self-hosted | Free | Yes |
Sanity + Next.js
Sanity is the CMS most beloved by developers in 2026. The GROQ query language is expressive in a way that SQL and GraphQL are not — the join syntax (author->{ name }) is particularly elegant compared to multi-step REST fetching. The Sanity Studio is built entirely in React, which means you can customize it with your own React components when your content model needs unusual interfaces.
The next-sanity package provides utilities specifically designed for Next.js App Router: a client configured for both published and draft content, a SanityImage component for optimized image rendering, and integration with Next.js Draft Mode for content preview.
// Sanity — schema definition
// schemas/post.ts
export default {
name: 'post',
title: 'Blog Post',
type: 'document',
fields: [
{
name: 'title',
title: 'Title',
type: 'string',
validation: (Rule) => Rule.required(),
},
{
name: 'slug',
title: 'Slug',
type: 'slug',
options: { source: 'title' },
},
{
name: 'content',
title: 'Content',
type: 'array',
of: [
{ type: 'block' }, // Portable Text
{ type: 'image' },
{ type: 'code' }, // code-input plugin
],
},
{
name: 'publishedAt',
type: 'datetime',
},
],
};
// Sanity — GROQ queries in Next.js App Router
import { createClient } from 'next-sanity';
import { groq } from 'next-sanity';
const client = createClient({
projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID!,
dataset: process.env.NEXT_PUBLIC_SANITY_DATASET!,
apiVersion: '2024-01-01',
useCdn: true,
});
// GROQ — Sanity's query language
const POSTS_QUERY = groq`
*[_type == "post" && !(_id in path('drafts.**'))] | order(publishedAt desc) {
_id,
title,
"slug": slug.current,
publishedAt,
"author": author->{ name, "image": image.asset->url },
"coverImage": mainImage.asset->url,
excerpt,
}
`;
// Fetch in Next.js App Router — runs on the server, no useEffect needed
async function getPosts() {
return client.fetch(POSTS_QUERY, {}, { next: { tags: ['posts'] } });
}
// app/blog/page.tsx
export default async function BlogPage() {
const posts = await getPosts();
return (
<div>
{posts.map(post => (
<article key={post._id}>
<h2>{post.title}</h2>
</article>
))}
</div>
);
}
// Sanity — live preview with Draft Mode
// app/api/draft/route.ts
import { draftMode } from 'next/headers';
import { redirect } from 'next/navigation';
export async function GET(req: Request) {
const { searchParams } = new URL(req.url);
const secret = searchParams.get('secret');
if (secret !== process.env.SANITY_PREVIEW_SECRET) {
return new Response('Invalid token', { status: 401 });
}
draftMode().enable();
redirect(searchParams.get('slug') ?? '/');
}
Sanity's free tier includes 3 users, 5GB assets, and 10,000 API requests per day — plenty for small projects. Paid plans start around $99/month for larger teams. The Content Lake API means all content is stored in Sanity's cloud infrastructure; there is no self-hosted option.
Contentful + Next.js
Contentful is the CMS that enterprises standardized on during 2015–2022, and many large organizations still run it today. Its strength is a mature content modeling interface, strong field validation, extensive webhooks, and a long track record of reliability at scale.
The SDK provides both REST and GraphQL APIs. The REST API is straightforward and well-documented; the GraphQL API is particularly useful when you have complex content models with many references and want to fetch exactly the fields you need in one request.
// Contentful — typed content fetching
import contentful from 'contentful';
const client = contentful.createClient({
space: process.env.CONTENTFUL_SPACE_ID!,
accessToken: process.env.CONTENTFUL_ACCESS_TOKEN!,
// For preview:
// host: 'preview.contentful.com',
// accessToken: process.env.CONTENTFUL_PREVIEW_TOKEN!,
});
// TypeScript types from Contentful CLI
import type { TypeBlogPost } from '@/contentful/types';
async function getBlogPosts(): Promise<TypeBlogPost[]> {
const entries = await client.getEntries<TypeBlogPost>({
content_type: 'blogPost',
order: ['-fields.publishDate'],
limit: 10,
'fields.publishDate[lte]': new Date().toISOString(),
});
return entries.items;
}
Contentful's TypeScript type generation (contentful-typescript-codegen or the newer CLI tool) is genuinely excellent — it reads your content model and generates TypeScript interfaces for all your content types. This means your fetched data is fully typed without any manual work.
// Contentful — webhook revalidation (Next.js ISR)
// app/api/contentful-webhook/route.ts
import { revalidatePath, revalidateTag } from 'next/cache';
export async function POST(req: Request) {
const secret = req.headers.get('x-contentful-webhook-secret');
if (secret !== process.env.CONTENTFUL_WEBHOOK_SECRET) {
return new Response('Unauthorized', { status: 401 });
}
const body = await req.json();
const contentType = body.sys.contentType?.sys.id;
if (contentType === 'blogPost') {
revalidateTag('blog-posts');
revalidatePath('/blog');
}
return new Response('OK');
}
The pricing is the main concern with Contentful. The free tier is generous (25,000 records, 2 locales, 5 users), but the jump to a paid plan starts at $300/month for teams that need more than the free tier allows. For many startups, this is a significant cost relative to self-hosted alternatives.
Payload CMS (Self-Hosted)
Payload is the TypeScript-native CMS that has grown explosively since its v2 release. The core idea is radical: you define your content model in TypeScript, and Payload generates the admin UI, REST API, GraphQL API, and TypeScript types automatically from that definition. No schema migrations, no generated code to commit — everything derives from your collection config.
Payload v3 (released in late 2024) embedded itself inside Next.js rather than running as a separate server. Your Payload instance runs inside your Next.js app — the admin panel is served at /admin, the API is served from Next.js Route Handlers, and data fetching in React Server Components can call Payload's local API directly without HTTP overhead.
// Payload — collection schema (TypeScript-first)
// collections/Posts.ts
import type { CollectionConfig } from 'payload';
export const Posts: CollectionConfig = {
slug: 'posts',
admin: {
useAsTitle: 'title',
defaultColumns: ['title', 'author', 'status', 'publishedAt'],
},
access: {
read: () => true, // Public
create: isAdmin,
update: isAdmin,
delete: isAdmin,
},
fields: [
{ name: 'title', type: 'text', required: true },
{ name: 'slug', type: 'text', unique: true, required: true },
{
name: 'content',
type: 'richText', // Lexical rich text editor
},
{
name: 'status',
type: 'select',
options: ['draft', 'published'],
defaultValue: 'draft',
},
{
name: 'author',
type: 'relationship',
relationTo: 'users',
},
{ name: 'publishedAt', type: 'date' },
],
hooks: {
beforeChange: [
({ data }) => {
if (data.status === 'published' && !data.publishedAt) {
data.publishedAt = new Date().toISOString();
}
return data;
},
],
},
};
// payload.config.ts — main Payload configuration
import { buildConfig } from 'payload';
import { Posts } from './collections/Posts';
import { Users } from './collections/Users';
import { postgresAdapter } from '@payloadcms/db-postgres';
export default buildConfig({
collections: [Posts, Users],
secret: process.env.PAYLOAD_SECRET!,
db: postgresAdapter({
pool: { connectionString: process.env.DATABASE_URL },
}),
admin: {
user: 'users',
},
});
// Next.js RSC — querying Payload's local API (no HTTP):
import { getPayload } from 'payload';
import config from '@/payload.config';
export default async function BlogPage() {
const payload = await getPayload({ config });
const { docs: posts } = await payload.find({
collection: 'posts',
where: { status: { equals: 'published' } },
sort: '-publishedAt',
});
return (
<ul>
{posts.map(post => <li key={post.id}>{post.title}</li>)}
</ul>
);
}
Because Payload generates TypeScript types from your collection config, the posts array above is fully typed — including all your custom fields — with zero manual type definitions. This is a significant advantage for teams that want type safety throughout their content layer.
The self-hosted nature means you bring your own database (PostgreSQL or MongoDB) and your own hosting. Vercel + Neon Postgres is a common production stack for Payload v3.
Keystatic (Git-Backed)
Keystatic stores content in your Git repository as YAML, JSON, or MDX files. There is no database, no external service, and no runtime cost. Content editors use a web UI that reads from and writes to your GitHub repository via the GitHub API.
This approach is ideal for content-heavy sites where the content model is relatively simple (blog posts, documentation, landing page copy) and the team is comfortable with Git-based workflows. Every content change is a Git commit, giving you a full audit trail and the ability to roll back to any previous state.
// Keystatic — content stored in your Git repo (no database)
// keystatic.config.ts
import { config, collection, fields } from '@keystatic/core';
export default config({
storage: {
kind: 'github', // 'local' for dev, 'github' for prod
repo: 'myorg/mysite',
},
collections: {
posts: collection({
label: 'Blog Posts',
slugField: 'title',
path: 'content/blog/*',
format: { contentField: 'content' },
schema: {
title: fields.slug({ name: { label: 'Title' } }),
publishedAt: fields.date({ label: 'Published At' }),
summary: fields.text({ label: 'Summary', multiline: true }),
content: fields.markdoc({ label: 'Content' }),
},
}),
},
});
Because the content is files in your repo, you read it with standard file system operations or Next.js's native MDX support — no API calls, no SDK, no network latency. This also means the content is available during the build step without any external dependencies, making it very resilient.
The limitation is that Keystatic is not suited for complex relational content (blog posts with authors who have profiles, nested content, user-generated content) or content that changes frequently. For a marketing site or a technical blog, it is hard to beat.
Comparison Table
| CMS | Type | Free Tier | Self-Hosted | Next.js DX | TypeScript |
|---|---|---|---|---|---|
| Sanity | Managed | 3 users, 5GB | No | Excellent | Yes |
| Contentful | Managed | 25K records | No | Good | Yes (CLI) |
| Payload | Self-hosted | Free | Yes | Excellent (v3) | Yes, native |
| Keystatic | Git-backed | Free | Yes | Good | Yes |
| Strapi | Self-hosted | Free | Yes | Good | Yes |
When to Choose
Sanity is the right choice for startups and development teams who want a polished editorial experience without enterprise pricing. GROQ is genuinely more expressive than SQL or GraphQL for content queries, and the React-based studio means you can build custom field types when needed.
Contentful makes sense when enterprise requirements drive the decision: SOC 2 compliance, SLAs, an established legal relationship, or an existing Contentful instance that the rest of the organization uses. The pricing is significant but justified at scale.
Payload CMS is the clear choice when you want zero vendor lock-in, need TypeScript types auto-generated from your schema, or are building a product where content management is deeply integrated with application logic (user-generated content, multi-tenant systems, complex access control).
Keystatic is ideal for documentation sites, blogs, and marketing sites where content is primarily flat files, the team is developer-led, and zero runtime cost is a priority.
Related Reading
- Compare Sanity and Contentful package metrics: /compare/sanity-vs-contentful
- Payload CMS package health: /packages/payload
- Best static site generators for content sites: /blog/best-static-site-generators-2026
See the live comparison
View sanity vs. contentful on PkgPulse →