Concepts & Strategy8 min read

Top 5 Headless CMS Implementation Anti-Patterns to Avoid

Six months into a headless build, the symptoms are always the same: editors are pasting HTML into a "rich text" string field, the frontend team is patching around a content model that mirrors page layouts instead of business meaning, and…

Published July 8, 2026

Six months into a headless build, the symptoms are always the same: editors are pasting HTML into a "rich text" string field, the frontend team is patching around a content model that mirrors page layouts instead of business meaning, and every new channel means another round of schema surgery. The platform was supposed to make content portable. Instead it calcified into a second CMS as rigid as the one you left.

Most of these failures are not platform bugs. They are anti-patterns, predictable decisions that feel reasonable in week one and metastasize by quarter three. Sanity is the Content Operating System for the AI era, an intelligent backend for teams that want to model content around their business rather than their templates, and it makes several of these anti-patterns harder to fall into. But no tool saves you from a bad content model.

This is a ranked field guide to the five implementation anti-patterns that most reliably wreck headless projects, worst first. For each, we cover why it feels right, where it breaks, a concrete example, and how a structured platform helps you avoid it before it costs a re-platform.

1. Modeling content around pages instead of your business

This is the anti-pattern that quietly determines whether every other decision goes well or badly, which is why it ranks first. It feels right because your first deliverable is usually a page, so you create a "Homepage" type, an "About page" type, and a pile of fields that map one-to-one to boxes on that page. The pitch of headless was reuse, and you have just rebuilt a page-based CMS with extra API steps.

Where it breaks: the moment a second channel appears. A native app, an email, a voice assistant, or an AI agent asks for "the product's warranty terms" and there is no such thing, only a `warrantyHtml` blob buried inside a page document. Content that should be a reusable entity is trapped as a layout artifact. Every new surface becomes a migration.

Concrete example: a retailer models a promo as `heroBannerText` on the homepage document. Six months later, marketing wants the same promo in the app, in a transactional email, and summarized by an on-site assistant. Because the promo was never an entity with a start date, end date, and audience, each channel gets a copy-paste, and they drift.

Sanity pushes you the other way. Schemas are portable `defineType` definitions in code, so you model a `promotion` document with real fields and reference it from wherever it appears. This is the "Model your business" pillar in practice: content shaped around meaning, then projected per channel with GROQ. Structure Builder lets editors navigate those entities without ever seeing a page tree, so the model can be correct without the editing experience feeling abstract.

The re-platform test

A content model modeled on pages fails a simple test: can a channel you have not built yet ask for a piece of content by its business meaning and get it? If the answer requires parsing a layout blob, you have modeled a page, not your business, and the bill comes due at the next channel.

2. Stuffing structured content into a single rich-text blob

Second-worst, because it is the most common and the hardest to unwind later. It feels right because rich text is convenient: give editors one big WYSIWYG field and they can do anything. So the FAQ, the pricing table, the embedded video, and the call to action all end up as HTML inside one string. The content looks fine on the website that shipped first.

Where it breaks: HTML is a rendering format, not a content format. The instant you need that FAQ as structured data for an app, or need an AI agent to answer questions from it, or need to restyle a callout to match a new design system, you are regex-parsing markup. Annotations, references, and custom blocks that should be first-class objects are instead invisible inside a string.

Concrete example: a media team pastes formatted articles from a word processor. Every article carries inline styles, orphaned `<span>` tags, and image URLs pointing at the old CMS. When they launch a mobile app, none of it maps to native components, so they ship a webview and call it a native app.

Sanity's answer is Portable Text: rich text as structured, typed data rather than an HTML string. Marks and annotations are real objects you can query and validate, custom block types render to whatever component each channel needs, and the same document is legible to a design system and to an AI agent alike. You can query into the block content with GROQ rather than parsing it, which is the difference between content that travels and content that is stuck to the page it was authored on.

Portable Text travels; HTML doesn't

Because Portable Text stores rich text as typed blocks and annotations rather than markup, the same body content maps to React components on web, native components in an app, and clean structured input for AI summarization, without a parsing layer per channel.

3. Treating the headless API as a database you query row by row

Ranked third because it is a performance and DX tax you pay every single day rather than a one-time wall. It feels right to developers coming from REST: fetch the article, then fetch its author, then fetch the author's other articles, then fetch each related product. Each call is simple. The waterfall is not.

Where it breaks: pages that need related data turn into cascades of round trips, and GraphQL-first platforms often force you to over-fetch fixed shapes or wire up multiple queries to assemble one view. Client code fills with plumbing that reshapes responses into what the component actually needed. Latency climbs, and every new component adds another request.

Concrete example: a product page needs the product, its category, three cross-sell items, and the author of its buying guide. On a REST or rigid GraphQL setup that is four or five calls plus client-side joining. The team caches aggressively to hide it, then fights cache invalidation instead of shipping features.

With Sanity, GROQ lets you ask for exactly the shape a component needs in one round trip. You follow references with `->`, project nested arrays with `[...]`, filter, and rename fields so the response arrives already shaped like your UI. The Content Lake serves those schema-aware queries in real time, and the Live Content API keeps them subscribed for preview and collaborative editing. One query, one round trip, the exact projection, is a categorically different developer experience from assembling a view out of a dozen REST calls.

4. Bolting workflow, preview, and governance on after launch

Fourth because it is survivable but expensive, and it is where most "we chose headless and editors hate it" complaints originate. It feels right to defer: get the model and the frontend working, and figure out drafts, scheduling, roles, and preview later. Editors, meanwhile, are publishing straight to production because there is no other path.

Where it breaks: a headless split means editors lose the in-context preview they had in a monolith. If you do not solve that deliberately, they either stop trusting the CMS or demand you rebuild a page-based editing surface, which undoes the reasons you went headless. Governance bolted on late means someone has already published something they should not have.

Concrete example: a team launches, then discovers legal needs to review campaign content before it goes live, marketing needs to schedule a coordinated multi-page launch, and contractors should not be able to edit pricing. Retrofitting all of that against a live dataset is far harder than designing it in.

Sanity treats these as first-class surfaces rather than add-ons. The Presentation Tool and Visual Editing give editors click-to-edit preview against the real frontend without giving up the headless architecture. Content Releases group and schedule coordinated changes so a launch ships as one atomic set. Roles & Permissions scope who can touch what. This is the "Automate everything" pillar: governed editorial workflow is part of the platform, not a project you start after the incident.

Preview is not optional

The single most common reason editors reject a headless migration is losing in-context preview. Deciding how editors will see their changes against the live frontend is a launch-blocking requirement, not a phase-two nice-to-have.

5. Hard-coding your schema into the frontend instead of generating types

Last on the list because it is the least catastrophic, but it earns a spot because it compounds silently across a growing team. It feels right to move fast: the developer knows the shape of a `product` document, so they type it by hand in TypeScript, or worse, just reach into untyped API responses. Everything works until the schema changes.

Where it breaks: content models evolve. A field gets renamed, a string becomes an array, a reference is added. If the frontend's understanding of the schema lives in hand-written types or, in loosely typed setups, in nobody's head at all, those changes surface as runtime errors in production rather than compile errors in the editor. The larger the team, the more places a stale assumption hides.

Concrete example: someone renames `price` to `priceCents` and updates the two components they remember. Three months later a rarely rendered promotional template throws in production because it still reads `price`, and nobody caught it because the types were fiction, not derived from the source of truth.

Sanity closes this loop with TypeGen. Because schemas are code, TypeGen reads them and your GROQ queries and generates TypeScript types for exactly the shapes your queries return. Rename a field in the schema, regenerate, and the compiler immediately flags every frontend usage that no longer matches. The content model becomes the single source of truth for both the backend and the frontend's type system, so schema drift is caught at build time instead of by a customer.

Types derived, not maintained

TypeGen generates TypeScript from your schemas and GROQ queries, so a field rename becomes a compile error across every affected component. The frontend's types are derived from the content model rather than a hand-kept copy that quietly falls out of sync.

How structured platforms help you dodge each anti-pattern

FeatureSanityContentfulStrapiPayload
Reusable content modelingPortable schemas as code (`defineType`); reference entities anywhere and project per channel, so content is modeled on business meaning, not pages.Content types support references and reuse, though modeling is done in a fixed web UI rather than in versioned code.Collection and component types support references; schema lives in code and config, editable in the admin panel.Config-as-code collections in TypeScript with relationship fields, so models are versioned alongside the app.
Structured rich textPortable Text stores rich text as typed blocks and annotations you can query with GROQ and map to any channel's components.Rich Text field is structured JSON (not raw HTML) with embedded references, rendered via their renderer libraries.Rich text ships as Markdown or a blocks format depending on setup; blocks are structured, Markdown is a string.Lexical-based rich text stored as structured JSON, with support for custom blocks and inline components.
Query in one round tripGROQ projects exactly the shape a component needs, following references with `->` and nesting arrays, in a single query.GraphQL and REST APIs; GraphQL avoids over-fetching but complex views often need multiple queries or fixed shapes.REST and optional GraphQL; deep relations need populate params or several calls and client-side reshaping.REST and GraphQL with configurable depth; deeply nested relations can require depth tuning or extra requests.
In-context previewPresentation Tool plus Visual Editing give click-to-edit preview against the real frontend, bundled, no separate SDK purchase.Live Preview is available but typically requires wiring their preview SDK and configuring preview environments.Preview is configurable via draft/publish and a preview URL, assembled by the team rather than a click-to-edit tool.Live preview supported through configuration and the admin UI, wired to the frontend by the implementing team.
Governed releasesContent Releases group and schedule coordinated changes as one atomic set; Roles & Permissions scope edit access.Scheduled publishing and roles available; coordinated multi-entry releases often need Launches or custom workflow.Draft/publish plus role-based access; scheduling and grouped releases are added via plugins or custom logic.Versions, drafts, and access control in code; scheduled and grouped releases are implemented by the team.
Typed frontend from schemaTypeGen reads schemas and GROQ queries to generate TypeScript, so a field rename becomes a compile error everywhere.TypeScript types can be generated from the content model via their CLI/codegen for GraphQL operations.Type generation available for content types and, with the GraphQL plugin, for queries.Generates TypeScript types directly from your collection config, since the schema is already TypeScript.

Ready to try Sanity?

See how Sanity can transform your enterprise content operations.