How do you convert rich text when moving between CMS platforms?
You convert rich text by turning the old format into a structured tree, transforming that tree node by node, and writing it out in the new CMS's format. Every serious rich text migration follows that shape, whether it runs as one script or a pipeline of small ones.
The hard part isn't the common case. Paragraphs, headings, bold and links convert cleanly with any decent library. The hard part is the long tail: an iframe pasted five years ago, a table someone built with line breaks, a footnote plugin's markup. These are what turn a quick script into a long project, so find them early.
This guide is the developer's view of one step in CMS content migration. It assumes you already have an export, from something like a Webflow CMS export, and a target content model.
What rich text formats do CMS platforms use?
Visual builders and WordPress store rich text as HTML, while most headless CMSs store it as structured JSON. Moving from HTML to JSON is a parsing job; moving from JSON to HTML is a rendering job, and usually easier.
Platform | Stored format | Notes |
|---|---|---|
WordPress | HTML with block comments | Shortcodes and page-builder markup need resolving; the REST API's rendered HTML helps |
Webflow | HTML | CSV export includes HTML; custom embeds and classes need handling |
Framer | Formatted text, imported from HTML | Keeps standard formatting; drops arbitrary classes and scripts |
Sanity | Portable Text (JSON) | Blocks, spans, marks and custom types you define |
Contentful | Rich Text (JSON) | Fixed node types; embedded entries and assets for anything custom |
Payload | Lexical editor state (JSON) | Configurable features; converters for HTML and Markdown available |
Strapi | Blocks (JSON) or Markdown | Depends on the field type chosen |
Check the target's documentation for the exact schema before building anything. Our headless CMS migration guide covers how these platforms differ beyond rich text.
How does a rich text migration between CMS platforms work?
A rich text migration works as a pipeline: parse, normalize, map, serialize and validate. Keep each stage separate so you can test and re-run them without touching the others.
Parse into a tree
Parse HTML with a real parser, such as the unified ecosystem's rehype for HTML or remark for Markdown. Never transform raw HTML strings with regular expressions.
Normalize
Remove empty paragraphs, inline styles and wrapper divs; turn b into strong and i into em; flatten spans that carry only classes.
Map node types
Apply your mapping table: headings to heading blocks, lists to list blocks, links to link marks or annotations, images to image blocks, embeds to custom blocks.
Handle assets
Download inline images, upload them to the new CMS, and replace URLs with the new asset references.
Serialize
Write the tree out as the target format: Portable Text, Contentful Rich Text, Lexical JSON, Markdown or clean HTML.
Validate
Check the output against the target schema, then render it and compare it with the original page.
Write the mapping table before the code. It's the spec your script implements, and the document you'll argue over with editors about what happens to their odd formatting.
How do you convert HTML to Portable Text?
Convert HTML to Portable Text with Sanity's block tools, which provide an htmlToBlocks function, and pass it rules for any element that isn't a standard block. Standard paragraphs, headings, lists and marks convert out of the box; everything else needs a rule.
Rules match an HTML element and return a Portable Text block, so an iframe can become a custom embed object and a figure with a caption can become an image block with a caption field. Define those custom types in your Sanity schema first, or the content will fail validation.
Contentful and Payload follow the same idea with different tools. Contentful has an official library for converting Markdown into its Rich Text format, so a common route is HTML to Markdown to Rich Text. Payload's Lexical editor ships converters for HTML and Markdown. See our guides to Contentful to Payload and Sanity to Payload for platform-specific details.
How do you convert rich text to Markdown and back?
Convert HTML to Markdown with a converter such as Turndown or rehype-remark, and Markdown to HTML with remark and rehype. Markdown is a good intermediate format because it's readable and diffable, but it can't express everything HTML can.
- What survives. Headings, paragraphs, emphasis, links, lists, images, blockquotes and code.
- What needs an extension. Tables and footnotes need GitHub-flavored Markdown support on both ends.
- What doesn't survive. Colors, alignment, column layouts, classes and arbitrary embeds. Keep them as raw HTML or convert to custom components.
- What to decide. Whether line breaks inside paragraphs matter, and how nested lists should indent.
If your target is a visual CMS, you may not need Markdown at all. Importing into Framer CMS takes clean HTML directly, as does Webflow to Framer CMS migration.
Which edge cases break rich text migrations?
Embeds, tables, footnotes, inline images and anything built with custom classes break most often. Find them before you write the converter by searching the export for each pattern and counting how often it appears.
Edge case | What goes wrong | How to handle it |
|---|---|---|
Iframes and video embeds | Dropped or turned into plain URLs | Map to a custom embed block with provider and URL |
Tables | Unsupported in some targets | Use a table block or plugin, or convert to a custom component |
Footnotes | Plugin markup becomes stray numbers | Convert to a footnotes field or inline notes |
Inline images | Old CDN URLs, missing alt text | Re-host, rewrite URLs, carry alt and captions |
Nested lists | Flattened or mis-indented | Test lists two and three levels deep |
Custom callouts and buttons | Become plain paragraphs | Map classes to custom blocks |
Code blocks | Lose language or whitespace | Map to a code block with a language field |
Hypothetical example: a blog full of pasted embeds
For example, suppose a startup blog on Webflow uses custom code embeds for videos, tweets and a newsletter signup box, and it's moving to Sanity. A search of the export shows three embed patterns plus a handful of one-offs.
The team defines three custom Portable Text types: video, social post and signup. The converter maps each known pattern to its type, logs the one-offs for manual review, and fails loudly on anything else. Editors then fix the logged posts by hand. This is illustrative, not a client result.
How do you validate converted rich text?
Validate converted content in two ways: automatically against the target schema, and visually by rendering old and new side by side. Schema checks catch broken structure; visual checks catch content that's valid but wrong.
Pick a test set on purpose: the longest posts, the oldest posts, and every post your edge-case search flagged. Compare word counts per post as a quick signal of dropped content, then check those with big differences by eye. Also check that the new templates render every custom block, since a block with no renderer shows nothing.
Rich text is also where content modeling decisions show up. If editors build pages with free-form rich text, consider structured page builder blocks and a cleaner content model in the new CMS.
