Why Portable Text?
Most systems store block content as an HTML string or Markdown. Both work, but both make assumptions about where and how your content will be displayed.
Portable Text takes a different approach: it stores block content as structured JSON data. No rendering assumptions. No parser needed. Just data you can query, transform, and render however you want.
The problem with HTML strings
Section titled “The problem with HTML strings”An HTML string looks like this:
<p>Read the <a href="/docs">documentation</a> for <strong>details</strong>.</p>This works if you’re rendering to a web page. But what if you need to:
- Render the same content in a mobile app (React Native, Flutter)?
- Generate a PDF or email?
- Extract plain text for search indexing?
- Query which documents contain links to a specific URL?
With an HTML string, every one of these requires parsing HTML. And HTML parsers are fragile. They break on malformed tags, handle edge cases differently across languages, and can’t distinguish between “a paragraph with a link” and “a div with an anchor tag” without understanding the full DOM context.
The problem with Markdown
Section titled “The problem with Markdown”Markdown is simpler than HTML, but it has its own limits:
- No custom block types. You can’t represent an image gallery, a call-to-action, or an embedded video in standard Markdown. Extensions exist, but they’re not portable across parsers.
- No inline structured data. You can’t embed a stock ticker, a product reference, or a mention that carries structured data inside a paragraph. Markdown is text all the way down.
- No data-carrying annotations. A Markdown link is
[text](url). If you need a link with a target, a tooltip, tracking parameters, and an internal reference ID, you’re back to embedding HTML. - No queryable structure. “Find all documents that mention this product” requires parsing every Markdown file.
The problem with block-based HTML (Gutenberg)
Section titled “The problem with block-based HTML (Gutenberg)”WordPress Gutenberg introduced block-based editing, which is the right idea: content as a sequence of typed blocks (paragraphs, images, embeds) rather than one big HTML string. But Gutenberg stores blocks as HTML comments inside an HTML string:
<!-- wp:heading --><h2 class="wp-block-heading">Hello world</h2><!-- /wp:heading -->
<!-- wp:image {"id":123,"sizeSlug":"large"} --><figure class="wp-block-image size-large"> <img src="photo.jpg" alt="A mountain landscape" /> <figcaption>View from the summit</figcaption></figure><!-- /wp:image -->The block metadata ({"id":123,"sizeSlug":"large"}) is embedded in an HTML comment. To access it, you parse HTML. To query which posts contain an image with a specific ID, you search HTML strings. To render the same content in a mobile app, you parse the HTML and extract the comment data.
Gutenberg has the right mental model (structured blocks), but the storage format (HTML with comments) inherits the same problems as plain HTML strings: parsing is fragile, querying is impractical, and rendering in a different format requires understanding the full HTML structure.
This isn’t a theoretical problem. EmDash (Cloudflare’s open-source CMS) built a dedicated converter, @emdash-cms/gutenberg-to-portable-text, that transforms 30+ Gutenberg block types into Portable Text. The converter exists because extracting structured content from Gutenberg’s HTML-comment format requires parsing every block type individually. With Portable Text, the same content is already queryable JSON.
How Portable Text works
Section titled “How Portable Text works”Portable Text takes the block model and stores it as JSON. The same content:
[ { "_type": "block", "style": "normal", "children": [ {"_type": "span", "text": "Read the "}, { "_type": "span", "text": "documentation", "marks": ["a1b2c3"] }, {"_type": "span", "text": " for "}, { "_type": "span", "text": "details", "marks": ["strong"] }, {"_type": "span", "text": "."} ], "markDefs": [ { "_key": "a1b2c3", "_type": "link", "href": "/docs" } ] }]More verbose? Yes. But now you can:
- Render anywhere. Pass the same data to a React component, an HTML serializer, a PDF generator, or a Slack message formatter. Each renders what it can, ignores what it can’t.
- Query the content. “Find all blocks that contain a link to /docs” is a JSON query, not a regex over HTML.
- Extend without breaking. Add a
callToActionblock type, afootnoteannotation that carries citation data, or astockTickerinline object with symbol and exchange fields inside a text paragraph. Renderers that don’t know about them skip them gracefully. No parser changes needed. - Validate the structure. The schema is explicit. You know exactly what types of content exist, what data they carry, and where they can appear.
What makes it portable
Section titled “What makes it portable”The “portable” in Portable Text means two things:
-
Portable across renderers. The same content renders as React components, HTML strings, Vue templates, Svelte components, PDFs, or plain text. Official serializers exist for React, HTML, Vue, Svelte, Astro, and more.
-
Portable across systems. Portable Text is a specification, not a library. Any system can produce it, any system can consume it. Sanity uses it as its native rich text format. EmDash (Cloudflare’s open-source CMS) adopted it as its core content format. Hugo has a built-in
transform.PortableTextfunction.
When to use Portable Text
Section titled “When to use Portable Text”Portable Text is a good fit when:
- Content needs to render in more than one format or platform
- You want to mix rich text with structured data (images with metadata, CTAs with tracking, embeds with configuration)
- Content is queryable (you need to find, filter, or analyze what’s inside rich text fields)
- Multiple teams or systems consume the same content
If you’re building a blog with a single web frontend and no custom block types, Markdown is probably fine. Portable Text pays off when content is reused, queried, or rendered in multiple contexts.
Next steps
Section titled “Next steps”Ready to use Portable Text?
- Render Portable Text if you have PT content and need to display it
- Build a rich text editor if you want to create an editing experience
- Read the specification for the full technical details