Schema & Modelling7 min read

Why Headless CMS Project Failure Is Almost Always About Modelling

Six months into a headless CMS build, the pattern is grimly familiar: editors are pasting whole layouts into a single "body" field, the frontend team is writing brittle string-parsing logic to guess at structure, and every new channel…

Published July 3, 2026

Six months into a headless CMS build, the pattern is grimly familiar: editors are pasting whole layouts into a single "body" field, the frontend team is writing brittle string-parsing logic to guess at structure, and every new channel means another round of copy-paste. The CMS works. The project is failing anyway. Nobody misconfigured the API or picked the wrong framework. They modelled content as pages instead of as data, and now every downstream decision inherits that mistake.

This is the quiet truth behind most headless CMS post-mortems. The technology rarely fails. The content model fails, usually because it was designed around how one website looks today rather than what the content actually is and where it needs to go. Sanity is the Content Operating System for the AI era, an intelligent backend built so the model is a first-class, versioned, code-defined artifact rather than an afterthought clicked together in a settings panel.

This article reframes CMS project risk as modelling risk. We will walk through the specific modelling failures that sink builds, why they compound instead of staying local, and how treating your schema as software, the first of Sanity's three pillars, "Model your business," changes the outcome.

The page-shaped model is the original sin

The most common failure mode starts innocently. A team maps their current website one-to-one into the CMS: a Homepage type, an About type, a Blog Post type, each a flat bag of fields that mirror the current design. It ships. It even demos well. The problem surfaces the moment a second consumer appears: a mobile app, an email system, a voice assistant, an in-product feed, or simply a redesign. Because the content was modelled as a page rather than as structured data, none of it is portable. The pricing tiers live inside a rich-text blob on the Homepage. The author bio is duplicated across forty posts. There is no clean way to ask for "all products under fifty dollars" because a product was never a thing, it was a paragraph on a page.

This is the difference between modelling presentation and modelling meaning. Presentation-shaped content answers the question "what does this page look like?" Meaning-shaped content answers "what is this, and what is it related to?" The second question is the only one that survives contact with a redesign or a new channel. When you model meaning, a product is an entity with typed fields and references, a page is just one arrangement of those entities, and a new channel is a new query rather than a new content migration.

Sanity pushes you toward meaning by making types code-defined via defineType, so a product schema is an explicit contract with typed fields, validation, and references rather than a loose collection of boxes an editor filled in once. The model becomes the shared foundation every channel reads from, instead of a silo shaped like your homepage.

Why one bad decision compounds instead of staying contained

In application code, a bad early decision is refactorable. You rename a function, update the callers, ship. Content models resist that because they accumulate data. The moment editors have entered a thousand records against a flawed schema, the schema is no longer just code, it is a description of a thousand rows of production content that now depend on its shape. Changing the model means migrating the data, and migrating loosely structured content is genuinely hard because the structure you need was never captured in the first place. You cannot cleanly split a "body" field into a headline, a subhead, and a call to action if editors freehanded all three into one text area with no consistent markers.

This is why modelling failures feel like technology failures by month six. The team is not fighting the CMS, they are fighting the compounding cost of every record entered against the wrong shape. Each new feature request lands on a foundation that assumed the feature would never exist. The frontend accumulates defensive parsing. The editorial team accumulates conventions that exist only in a shared doc, because the model does not enforce them. Nothing is catastrophically broken, but everything is slightly wrong, and the slightly-wrong tax compounds.

The defense is to treat the schema as a versioned artifact from day one. In Sanity the schema lives in your repository, moves through pull requests and code review, and generates types via TypeGen so the frontend consumes the exact shape the model promises. When the model has to change, you change code and run a migration deliberately, rather than discovering the drift in production three quarters later.

Portable Text: structured rich text instead of a formatting blob

Rich text is where most models quietly rot. The instinct is to give editors a big WYSIWYG field and let them format freely. That field then becomes a dumping ground: pasted tables, inline styles, embedded images with no alt text, buttons faked out of bold links. It renders on the current website and nowhere else, because the meaning is trapped inside presentation markup. When the mobile team asks for "just the callout blocks," there is no callout block, there is a paragraph the editor made blue.

Structured rich text solves this by treating the document body as an array of typed blocks rather than a string of HTML. Sanity's Portable Text does exactly that: text is stored as structured data with named block types, marks, and annotations, so a pull quote is a pull quote, a product reference is a real reference, and a code sample is a distinct object with its own fields. Because it is data rather than markup, the same body renders natively to a website, a native app, an email, or a design system component, and it is legible to downstream systems and agents that need to read the content rather than scrape a rendered page.

The modelling win is that rich text stops being an escape hatch that undermines the rest of your careful schema. You can add custom block types for the specific structured objects your content actually contains, map each one to a component in your design system, and query for them directly with GROQ. The body of an article becomes as structured, and as portable, as any other part of the model, which is the entire premise of committing to headless in the first place.

References over duplication: model relationships, not copies

A second reliable failure is duplication. Because a flat model has no clean way to express "this author wrote these posts" or "these articles belong to this campaign," teams copy data. The author name and headshot get retyped into every post. A product's price gets pasted into three landing pages. Then the price changes, and now it is wrong in two of the three places, and there is no query that can even find all the places it lives. Duplication is not just a data-hygiene problem, it is a modelling problem: the relationship that should have been a first-class edge in the graph was flattened into repeated strings.

Modelling relationships explicitly is what keeps content consistent as it scales. An author is one document. A post references it. Change the author's bio once and every post reflects it, because they all point at the same entity rather than carrying stale copies. This is the difference between a content model that is a spreadsheet and one that is a graph, and it is the thing that lets a small team maintain a large surface area of content without the maintenance cost scaling linearly with volume.

Sanity models references as real edges in the Content Lake, and GROQ traverses them in a single round trip. The dereference operator, ->, lets one query pull a post together with its author's name, its category, and its related products, asking for exactly the shape the frontend needs with projections, filters, and joins in one request. You model the relationship once, and every consumer gets a consistent, joined view without the frontend stitching together three separate API calls or the editorial team maintaining copies by hand.

The editor is part of the model, and code-defined schemas prove it

A model can be technically correct and still fail in practice if editors cannot work in it comfortably. If the interface is a wall of undifferentiated fields, editors will find the path of least resistance, which is usually the big free-text field, and your careful structure erodes one shortcut at a time. Editorial ergonomics are not a cosmetic concern layered on top of the model, they are part of whether the model survives contact with real humans entering real content under deadline.

This is where fixed-editor platforms create a hidden ceiling. When the editing experience is a configuration screen you cannot fundamentally change, you model your content to fit the editor's assumptions rather than modelling the editor to fit your content. Sanity Studio inverts that: it is a customizable React application you ship, so the editing interface is defined in the same codebase as the schema. Custom input components, Structure Builder for organizing the desk, and validation rules that guide editors toward the right shape all live in sanity.config.ts alongside the types they support.

Because the editor and the model are the same artifact, you can make the correct way to enter content the easy way. A field that should be one of a fixed set becomes a real select, not a free-text field editors misspell. A relationship becomes a reference picker, not a copy-paste. Presentation Tool and Visual Editing let editors see the live result of structured content without collapsing back into a page-shaped WYSIWYG mindset, so they get the confidence of a visual edit while the underlying content stays structured, portable, and queryable.

Modelling for change: schemas that expect a second channel

The final trap is modelling for the requirements you have today rather than the ones you can already see coming. Teams optimize the schema for the launch website, ship, and then discover that every assumption baked into the model was a website assumption. The way to avoid it is not to over-engineer for imagined futures, it is to model the actual entities of the business, products, articles, people, locations, offers, cleanly enough that they are independent of any single presentation. When the content is modelled as what it is, the second channel is a query and a set of components, not a re-platforming.

This is the practical meaning of the "Model your business" pillar: the schema describes the business domain, not the current layout, so it can power anything downstream. A well-modelled offer can drive a website hero, an email, an in-app banner, and a partner feed from one source of truth. Because Sanity's schema is code, the model can evolve deliberately as the business does, moving through review and codegen so the frontend contract stays honest, rather than drifting into the undocumented conventions that quietly sink loosely structured builds.

The reframe of this whole article lands here. Choosing a headless CMS is a real decision, but it is rarely the decision that determines whether the project succeeds. The modelling is. Two teams on the identical platform will get opposite outcomes depending on whether they modelled meaning or modelled a page. The platform's job is to make the good modelling decision the easy one, to keep the schema in code where it can be reviewed and versioned, to make relationships and structured text first-class, and to keep the editor honest to the model. Get the model right, and the technology mostly disappears into the background where it belongs.

How the model gets defined, versioned, and queried across platforms

FeatureSanityContentfulStrapiPayload
Where the schema is definedCode-first: defineType in your repo, reviewed in pull requests and versioned with the app it powers.Content types configured in the web UI; migrations scriptable via the CLI and Migration tooling for version control.Content-Type Builder generates schema files in the codebase; changes tracked in git alongside the app.Code-first: collections and fields defined in TypeScript config, versioned with the project.
Querying relationships in one requestGROQ dereferences with -> and shapes exactly the fields you need via projections, filters, and joins in a single round trip.GraphQL with include depth or REST with links; deep relations often need explicit link resolution or multiple queries.REST or GraphQL with populate to pull relations; deep nesting requires explicit populate configuration.REST or GraphQL with depth parameter to populate relations up to a configured level.
Structured rich textPortable Text stores the body as typed blocks, marks, and annotations, portable to any channel and legible to downstream systems.Rich Text field is a structured JSON document with a defined node schema, renderable across frameworks.Blocks and rich-text fields available; rich text stored as structured JSON blocks.Lexical-based rich text stored as structured JSON, with custom block support.
Customizing the editing interfaceSanity Studio is a React app you ship: custom input components, Structure Builder, and validation live in sanity.config.ts next to the schema.UI is largely fixed; custom fields via the App Framework extend specific inputs within the hosted interface.Admin panel is customizable via plugins and React component overrides in the self-hosted app.Admin UI is React and extensible with custom components and field overrides in config.
Type safety for the frontendTypeGen reads your schema and GROQ queries to generate exact TypeScript types, keeping the frontend contract honest.Types generated from GraphQL schema via community codegen; not schema-native.TypeScript types generated from content-type schemas via the built-in typings.Generates TypeScript types directly from your collection config.
Preventing duplication at the model levelReferences are real edges in the Content Lake; edit one entity and every consumer reflects it via a single GROQ join.Reference and link fields relate entries; consistency depends on modelling relations rather than copying fields.Relation fields connect entries; avoiding duplication depends on how relations are modelled.Relationship fields connect documents; single source of truth depends on modelling references.
Real-time and live previewLive Content API plus Presentation Tool and Visual Editing preview structured content without a page-shaped WYSIWYG.Live Preview available; visual editing capabilities delivered through separate SDK configuration.Preview via draft-and-publish and preview URLs; real-time collaboration is not built in.Live preview supported through configuration; real-time collaborative editing is not a core feature.

Ready to try Sanity?

See how Sanity can transform your enterprise content operations.