Skip to content

Web Development · Content Management

Headless CMS in 2026: Sanity, Payload, and Contentful for Agency Projects

Three headless CMS platforms used by agencies, three different bets on where content management is going. Here's how Sanity, Payload, and Contentful actually compare when you're building for clients.

Anurag Verma

Anurag Verma

7 min read

Headless CMS in 2026: Sanity, Payload, and Contentful for Agency Projects

Sponsored

Share

The headless CMS market fractured once developers started caring about it. Traditional CMS products (WordPress, Drupal) owned the space when the CMS and the frontend were the same thing. Once the frontend moved to React, Vue, and static site generators, the content management layer became its own decision.

Three platforms show up repeatedly in agency discussions: Sanity, Payload, and Contentful. They’re not competing on the same axis. Sanity is betting on real-time collaboration and structured content; Payload is betting on code-first schema definition and self-hosting; Contentful is betting on enterprise stability and ecosystem depth. Understanding which bet fits your project determines which tool you should pick.

Sanity

Sanity stores content in GROQ-queryable documents, defines schemas in JavaScript/TypeScript files, and ships a customizable editing interface called Sanity Studio. The Studio is a React application you can extend with custom input components, previews, and plugins.

Schema definition in Sanity is code:

// schemas/post.js
export default {
  name: 'post',
  title: 'Blog Post',
  type: 'document',
  fields: [
    {
      name: 'title',
      title: 'Title',
      type: 'string',
      validation: Rule => Rule.required().max(96)
    },
    {
      name: 'slug',
      title: 'Slug',
      type: 'slug',
      options: { source: 'title' }
    },
    {
      name: 'author',
      title: 'Author',
      type: 'reference',
      to: [{ type: 'author' }]
    },
    {
      name: 'body',
      title: 'Body',
      type: 'array',
      of: [
        { type: 'block' }, // portable text
        { type: 'image' }
      ]
    },
    {
      name: 'publishedAt',
      title: 'Published at',
      type: 'datetime'
    }
  ]
}

GROQ is Sanity’s query language. It’s similar to GraphQL in concept but purpose-built for document queries:

// Fetch published posts with author data
const posts = await client.fetch(`
  *[_type == "post" && publishedAt < now()] | order(publishedAt desc) {
    _id,
    title,
    slug,
    publishedAt,
    author-> {
      name,
      image
    },
    "excerpt": body[0].children[0].text
  }
`)

The -> operator dereferences a reference inline. Content modeling with deeply nested references stays readable.

Where Sanity works well:

  • Teams that want content editors and developers working simultaneously (real-time multiplayer editing)
  • Projects where the content model will evolve (schema changes deploy with code)
  • Rich content with complex structures (Portable Text handles nested components, custom blocks, embedded media)

Where it gets complicated:

  • Self-hosting is not available on the free or standard plans. Content data lives in Sanity’s cloud. If a client has data residency requirements, this is a blocker.
  • GROQ has a learning curve. Developers familiar with SQL or GraphQL will need time to internalize the filter/projection syntax.
  • Pricing is consumption-based and can surprise you on high-traffic sites (API requests and bandwidth).

Payload CMS

Payload is TypeScript-first, self-hostable, and defines everything in a single payload.config.ts file: schemas, access control, hooks, and custom endpoints. There’s no separate schema format to learn. If you know TypeScript, you already know how to configure Payload.

// payload.config.ts
import { buildConfig } from 'payload/config'
import { slateEditor } from '@payloadcms/richtext-slate'

export default buildConfig({
  collections: [
    {
      slug: 'posts',
      admin: {
        useAsTitle: 'title',
        defaultColumns: ['title', 'status', 'publishedAt'],
      },
      access: {
        read: () => true,
        create: ({ req }) => Boolean(req.user),
        update: ({ req }) => Boolean(req.user?.role === 'admin'),
      },
      fields: [
        { name: 'title', type: 'text', required: true },
        { name: 'slug', type: 'text', required: true, unique: true },
        {
          name: 'status',
          type: 'select',
          options: ['draft', 'published'],
          defaultValue: 'draft',
        },
        {
          name: 'content',
          type: 'richText',
          editor: slateEditor({}),
        },
        {
          name: 'author',
          type: 'relationship',
          relationTo: 'users',
        },
        { name: 'publishedAt', type: 'date' },
      ],
    }
  ],
  admin: {
    user: 'users',
  },
  editor: slateEditor({}),
  db: postgresAdapter({
    pool: { connectionString: process.env.DATABASE_URI }
  }),
})

Payload generates a REST API and a local API automatically from your config. The local API is particularly useful in monorepos or full-stack Next.js apps, letting you query the CMS directly without an HTTP round-trip:

// In a Next.js server component, no HTTP request needed
import { getPayloadHMR } from '@payloadcms/next/utilities'
import configPromise from '@payload-config'

const payload = await getPayloadHMR({ config: configPromise })

const posts = await payload.find({
  collection: 'posts',
  where: { status: { equals: 'published' } },
  sort: '-publishedAt',
  depth: 2, // populate relationship depth
})

Where Payload works well:

  • Self-hosting is a requirement (you run it on your own Postgres, on any VPS or serverless platform)
  • Full-stack TypeScript projects where you want the CMS schema colocated with your application code
  • Projects that need custom business logic in the CMS (hooks on create/update/delete, custom endpoints, field-level validation)

Where it gets complicated:

  • It’s an active codebase with breaking changes between major versions. Upgrading requires attention.
  • Hosting is your responsibility. Managed Payload is available but the main selling point is self-hosting.
  • The admin UI is functional but not as polished as Sanity Studio for non-technical editors.

Contentful

Contentful is the enterprise incumbent. It has been running in production for over a decade, has a large ecosystem of integrations, and offers stability that newer tools don’t match. Content modeling happens in the UI or via the management API, and you query content via REST or GraphQL.

// Contentful REST API with the SDK
import { createClient } from 'contentful'

const client = createClient({
  space: process.env.CONTENTFUL_SPACE_ID!,
  accessToken: process.env.CONTENTFUL_ACCESS_TOKEN!,
})

const posts = await client.getEntries({
  content_type: 'blogPost',
  order: ['-fields.publishedAt'],
  limit: 10,
  'fields.status': 'published',
})

// TypeScript types via contentful-typescript-codegen
posts.items.map((post) => ({
  title: post.fields.title,
  slug: post.fields.slug,
  // ...
}))

Contentful’s GraphQL endpoint is cleaner for complex queries with nested references:

query GetPosts($limit: Int, $skip: Int) {
  blogPostCollection(
    limit: $limit
    skip: $skip
    order: publishedAt_DESC
    where: { status: "published" }
  ) {
    total
    items {
      title
      slug
      publishedAt
      author {
        name
        photo {
          url
          width
          height
        }
      }
    }
  }
}

Where Contentful works well:

  • Clients who need stability and a proven track record for enterprise projects
  • Projects with large content teams who need a mature, polished editor experience
  • Integrations with existing enterprise tooling (Contentful has connectors for Salesforce, Marketo, various DAMs)

Where it gets complicated:

  • Pricing scales aggressively. The free tier is limited. Growth plans get expensive quickly for high-content, high-traffic sites.
  • Schema changes (content type changes) are done through the UI or API, not code. This makes them harder to version and review.
  • Content modeling in the UI can diverge from what developers expect without code reviews of the schema.

How to Choose

The decision usually comes down to three questions:

Can the client’s data live in someone else’s cloud? If no (data residency, compliance, client preference), Contentful and Sanity’s managed hosting are both off the table. Payload self-hosted is the answer.

How technical are the content editors? Sanity Studio and Contentful have better editing experiences for non-technical users. Payload’s admin is functional but more developer-facing. If the content team is large and not technical, this matters.

How often will the content model change? If the schema evolves with the product, Sanity and Payload both handle this well because schemas are in code and deploy with the application. Contentful schema changes through the UI creates drift.

A rough guide:

SanityPayloadContentful
Self-hostableNo (Sanity cloud)YesNo (Contentful cloud)
Schema in codeYesYesNo (UI-based)
Query languageGROQREST/Local APIREST/GraphQL
Editor experienceExcellentGoodExcellent
Free tierGenerousOpen sourceLimited
Best forContent-rich sites, collaborative editingFull-stack TypeScript apps, self-hostingEnterprise, large teams

For a typical agency project (a marketing site, a product blog, a documentation site), all three work. The tiebreaker is usually the client’s data requirements and whether you want to be responsible for hosting the CMS yourself.

Sponsored

Sponsored

Discussion

Join the conversation.

Comments are powered by GitHub Discussions. Sign in with your GitHub account to leave a comment.

Sponsored