Beyond HTML: Using Portable Text for Multi-Platform Content
Stop fighting HTML strings in your CMS. Learn how Sanity's Portable Text uses JSON to decouple content structure from presentation for multi-platform delivery.
06 Apr 2026, 04:47 UTC

The Problem with Bloated Rich Text
Most CMS platforms store rich text as a giant string of HTML. While this is easy to drop into a web page, it creates a technical wall when you need to push that same content to a mobile app, a smartwatch, or an email template. Parsing HTML on a native iOS or Android app is fragile and inefficient, and stripping tags often destroys the intended structure of the content.
The takeaway is simple: to maintain a single source of truth across different platforms, you need structured content rather than formatted text. This is where Sanity's Portable Text comes in.
What is Portable Text?
Portable Text is a JSON-based specification for rich text. Instead of storing a paragraph as <p>Hello</p>, it stores it as an array of objects. Each object defines the intent of the content—such as a heading, a list item, or a block of normal text—and the marks applied to it, such as bold or italic.
Because it is plain JSON, Portable Text is agnostic to the presentation layer. Your content remains a data structure that can be queried, filtered, and transformed without needing a DOM parser.
Mapping Data with Serializers
Front-end rendering is handled by official serializer packages (e.g., @portabletext/react) that map block types to React components. Custom serializers allow teams to support bespoke components like embeds or widgets. This ensures that while the data remains structured, the visual output is tailored to the specific framework of the target platform.
Example: Implementing a Custom CTA Block
Imagine you want to include a special "Call to Action" button within your blog body. You first define the type in your Sanity schema, then handle the rendering in your code.
// 1. Sanity Schema Definition (Studio)
export {
name: 'body',
type: 'array',
of: [
{ type: 'block' },
{
name: 'ctaBlock',
type: 'object',
fields: [
{ name: 'label', type: 'string' },
{ name: 'url', type: 'url' }
]
}
]
}// 2. Frontend Serializer (React)
import { PortableText } from '@portabletext/react';
const components = {
types: {
ctaBlock: ({ value }) => (
{value.label}
),
},
};
// Usage in your component
<PortableText value={body} components={components} /gt;Trade-offs and Limitations
The primary overhead is the development cost. Teams must write or maintain serializers for each target platform (web, mobile, email) to ensure consistent rendering. Additionally, large documents with many inline marks can increase payload size. To mitigate this, use GROQ projections when querying the field to fetch only the necessary nested data.
Verifying Your Implementation
To ensure your setup is working correctly, query your document using GROQ and inspect the JSON structure. Run *[_type == 'post']{title, body} in the Sanity Vision tool. If you see an array of objects with children and marks keys rather than an HTML string, your content is properly structured.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.