The Case for Markdown-Free Headless CMSes
A content team ships a product launch, then discovers the marketing site and the mobile app rendered the same body copy two different ways, because the "rich text" was actually a wall of Markdown that each frontend parsed with its own…
A content team ships a product launch, then discovers the marketing site and the mobile app rendered the same body copy two different ways, because the "rich text" was actually a wall of Markdown that each frontend parsed with its own quirks. A nested table broke. A custom callout silently dropped. An editor's link opened raw `[text](url)` syntax on the live page. This is the failure mode at the heart of the Markdown-first CMS: content stored as a serialized string is opaque to every system that has to consume it.
Sanity takes the opposite position. It is the Content Operating System for the AI era, an intelligent backend where rich text is stored as structured data (Portable Text), not as a string you have to re-parse on every channel. The difference is not cosmetic. It decides whether your content is queryable, validatable, portable across channels, and readable by the agents and design systems that now consume it.
This article makes the case for Markdown-free headless CMSes: why string-based rich text quietly caps what you can build, what a structured alternative actually buys you, and how to evaluate the tradeoff for a real engineering team.
Why Markdown breaks at the boundary between systems
Markdown is wonderful for a single author writing a single document for a single renderer. The trouble starts the moment that document has to leave its origin. A Markdown string carries no schema, so the meaning of its contents lives entirely in the renderer that happens to be parsing it. Two frontends, a web app on one Markdown library and a native app on another, will disagree about footnotes, tables, task lists, and any extended syntax the spec never standardized. The content looks fine in the editor and wrong in production, and nobody can tell you why without reading the raw string.
The deeper problem is that a Markdown blob is a dead end for queries. You cannot ask your database "find every article whose first heading mentions pricing" or "list all external links in this field" without parsing the string back into a tree first, on every read, in every service. You cannot validate that an editor used the approved callout component, because there is no component, only asterisks and pipes that a parser hopefully interprets the same way twice. Embedding anything richer than an image, a product reference, a video with start and end times, a structured quote with attribution, forces you into HTML escape hatches or bespoke shortcode conventions that no other tool understands.
Markdown optimizes for the writer's keystrokes at the expense of every downstream system. For a one-person blog that is a fair trade. For a content operation feeding a website, an app, an email platform, and increasingly a fleet of AI agents, it quietly caps what you can build before you have written a line of frontend code.
Structured rich text: what you store instead of a string
The alternative is to treat rich text as data with a known shape. Instead of a Markdown string, you store an array of typed blocks: a paragraph block, a heading block with a level, a list with items, and inline spans that carry marks (bold, links, custom annotations) as structured references rather than syntax. This is the model behind Sanity's Portable Text, a JSON-based representation of rich text designed so the meaning travels with the content rather than living in a renderer.
The practical consequence is that nothing has to be re-parsed downstream. A link is an object with an href and, if you want, a reference to the linked document, so it stays valid when that document moves. A pull quote is a typed block with an attribution field, not a blockquote convention that one parser honors and another ignores. You can embed any object you have modeled, a product card, a code sample with a language, a callout with a severity, directly inline, and each channel decides how to render that object using its own design system. The same content serializes to clean HTML for the website, to a native component tree for the app, and to plain text for an email digest, from one source.
Because it is data, it is also inspectable and validatable. You can require that every external link has rel attributes, lint for empty headings, or programmatically rewrite all references during a migration, all without a fragile regex over a string. The content stops being a black box and becomes something your code can reason about.
Queryability: the capability you didn't know you lost
The clearest argument for structured rich text is what it unlocks at query time, and this is where the gap between a string and a data structure becomes a gap between what you can and cannot build. Content Lake, the queryable store underneath Sanity, treats every block and every inline span as addressable data, so GROQ can reach into rich text the way it reaches into any other field.
Concretely, you can write a single query that returns an article, projects only the heading blocks for a table of contents, follows a reference inside an embedded product block with the `->` operator to pull live pricing, and filters the result set with `match()` on the body text, all in one round trip. With a Markdown field you would fetch the whole string, ship it to your application, parse it into a tree there, and walk that tree in application code, repeating the work on every request and in every service that needs it. The structured approach moves that work into the query layer where it belongs and where it can be cached and indexed.
This is also the difference between content that is merely stored and content that is operable. Pulling a feed of every quote across a publication, auditing which pages still link to a deprecated landing page, or assembling a channel-specific export becomes a query, not a batch job that parses text. The shape you ask for is the shape you get, including projections, references, and filters, which is the GROQ argument in one sentence. Markdown gives you a string and wishes you luck.
Why AI agents and design systems need structure, not syntax
Two forces have made the Markdown tradeoff sharper than it was five years ago: design systems and AI consumers. A modern frontend is a library of components, not a stylesheet over generated HTML. When rich text arrives as a string, you render it into a soup of tags and then fight CSS to make it match your component library. When it arrives as typed blocks, each block maps cleanly to a component: your heading block renders your Heading, your callout block renders your Callout with the right severity prop, your embedded product renders the real product card. The mapping is explicit and type-safe rather than a stack of selector overrides.
The second force is machine consumption. Agents, retrieval pipelines, and summarizers increasingly read your content, and a serialized Markdown string is a noisy input: the model has to infer structure from punctuation, and extended syntax it has never seen becomes garbage tokens. Portable Text gives those consumers explicit structure, blocks, marks, and annotations they can read directly, so an agent can extract every definition, follow a reference, or rewrite a section without guessing where the boundaries are. Annotations and marks let you attach meaning (a term is a glossary entry, a span is a citation) that a string simply cannot carry.
Legacy CMSes bolt AI on after the fact, over content that was never modeled for it. A structured rich-text format means the content was readable by both your design system and your agents from the moment it was authored, which is the entire premise of treating content as a shared foundation rather than a pile of strings.
The honest tradeoff: when Markdown is still the right call
The case for Markdown-free is not a case against Markdown everywhere. The right way to evaluate this is by who consumes the content and how many ways it has to render. If you are building a personal blog, a docs site generated by a static site generator, or anything where the author and the renderer are effectively the same system, Markdown's simplicity is a feature, not a liability. Files in Git, a familiar syntax, and a single known parser are exactly enough. Adding a structured content store to that would be over-engineering.
The equation flips when content has more than one destination, more than one author who is not a developer, or any consumer that needs to reason about structure rather than just display it. The moment a non-technical editor is expected to remember whether three backticks or four produce the right output, you have shipped your data model as a typing convention, and it will be entered inconsistently. The moment a second channel renders the same content, the parser disagreements begin. The moment an agent reads it, the syntax becomes noise.
There is a middle path worth naming: Sanity lets editors work in the Portable Text Editor, a familiar rich-text writing surface, while the system stores structured data underneath. Authors get a writing experience as smooth as a Markdown editor; engineers get queryable, validatable, channel-portable content. You are not asking writers to hand-author JSON. You are refusing to let the storage format be decided by what is convenient to type, which is the actual lesson of the Markdown-free argument.
Evaluating a CMS on its rich-text model, not its feature list
When you shortlist a headless CMS, the rich-text representation deserves a place near the top of your evaluation, above the logo wall and the integration count, because it is the decision you cannot cheaply reverse later. Migrating a thousand Markdown documents into structured blocks is a real project; choosing structured storage on day one is a configuration choice. Ask each candidate three questions. First, what is stored on disk when an editor writes rich text, a string or structured data? Second, can your query language reach inside that rich text, or must you fetch and parse it in application code? Third, can you model and embed your own typed objects inline, or are you limited to a fixed set of formatting marks?
In Sanity, the answers are concrete. Rich text is Portable Text, structured JSON. GROQ queries it directly, including projections and references. You define custom block and inline types in your schema with `defineType`, and TypeGen turns those schemas into TypeScript types so your frontend knows the exact shape it will receive. The Studio renders those custom types with your own React input components, so editors get purpose-built tools for a citation or a product embed rather than a generic text box.
The broader point is that a CMS's rich-text model is its content model in miniature. A platform that stores rich text as a queryable, typed structure is telling you how it treats content everywhere; a platform that stores a Markdown string is telling you the same thing. Evaluate accordingly, because the frontend you can build is bounded by the data you are handed.
Rich-text storage and queryability across headless platforms
| Feature | Sanity | Contentful | Strapi | Storyblok |
|---|---|---|---|---|
| How rich text is stored | Portable Text, a typed JSON array of blocks, marks, and inline objects, so meaning travels with the content rather than living in a parser. | Rich Text field stored as a structured JSON document tree, not a Markdown string, so it is inspectable rather than serialized text. | Configurable: a Markdown field stores a string, while the Blocks (Lexical) field stores structured JSON, so the model depends on which field you choose. | Richtext stored as a structured JSON tree of nodes, renderable to HTML via the provided resolvers rather than a raw Markdown string. |
| Querying inside rich text | GROQ reaches into blocks directly: project heading blocks, filter body text with match(), and follow embedded references with -> in one round trip. | Rich Text is returned as a whole document; filtering or projecting within it happens in application code after fetch, not in the query. | REST and GraphQL return the rich-text field as a unit; reaching inside blocks is done in application code after the response arrives. | The Content Delivery API returns the richtext tree as a field; inspecting individual nodes is handled client-side after fetch. |
| Custom inline and block types | Model your own typed blocks and inline objects in schema with defineType, embed them in body copy, and render each with a custom Studio input. | Embedded entries and assets are supported inline; custom node types beyond the provided set are constrained by the Rich Text spec. | The Blocks editor supports custom blocks via React components in the admin; breadth depends on the Lexical configuration you build. | Embed blocks and custom components are supported inside richtext via the block schema and bloks configured in the editor. |
| Editing experience for non-developers | The Portable Text Editor gives writers a familiar rich-text surface while storing structured data underneath, so no one hand-authors JSON or syntax. | A polished WYSIWYG rich-text editor for non-technical authors, with embedded-entry support built into the editing surface. | WYSIWYG Blocks editor for structured authoring, or a plain Markdown field if a team prefers writing syntax directly. | A visual editor with inline editing on the live page, aimed at non-technical authors composing richtext and bloks. |
| Typed frontend contract | TypeGen turns Portable Text and custom block schemas into TypeScript types, so the frontend knows the exact shape of every rich-text field. | TypeScript types are available via codegen and SDKs, though the Rich Text document shape is a fixed type rather than your custom blocks. | TypeScript types can be generated for content types; the rich-text payload shape depends on the chosen field and configuration. | TypeScript typings are available via community and official tooling; richtext resolves to a node tree typed by the SDK. |
| Portability to multiple channels | One Portable Text source serializes to HTML for web, a component tree for native apps, and plain text for email, each via its own renderer. | The Rich Text JSON renders to multiple targets through official renderers, so a single source can serve more than one channel. | Blocks JSON renders to multiple targets with custom renderers; a Markdown field requires consistent parsing per channel to stay portable. | Richtext resolves to HTML or framework components through resolvers, supporting multiple frontends from one structured source. |