All posts
Platform migrations

Migrating Rich Text Between CMSs: HTML, Markdown and Portable Text

Rich text migration between CMS platforms: how to convert HTML, Markdown, Portable Text and Lexical JSON, plus the embeds, tables and footnotes that break.

By Mehdi Hassan7 min read

The short answer

A rich text migration between CMS platforms works by parsing the source format (usually HTML) into a syntax tree, mapping each node type to the target's equivalent, and serializing it into the new format, such as Portable Text, Contentful Rich Text, Lexical JSON or Markdown. Embeds, tables, footnotes and inline images need explicit rules or they're lost.

Key takeaways

  • Convert through a syntax tree, never with regex find-and-replace on raw HTML.
  • Write a mapping table for every node and mark before writing code: headings, lists, links, images, embeds.
  • Anything the target can't represent natively becomes a custom block, not plain text.
  • Re-host inline images during conversion and rewrite their URLs in the same pass.
  • Validate by rendering old and new side by side on a sample of the longest, oddest posts.

Part of CMS Content Migration: How to Export, Clean and Import Content Between Platforms

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.

  1. 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.

  2. Normalize

    Remove empty paragraphs, inline styles and wrapper divs; turn b into strong and i into em; flatten spans that carry only classes.

  3. 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.

  4. Handle assets

    Download inline images, upload them to the new CMS, and replace URLs with the new asset references.

  5. Serialize

    Write the tree out as the target format: Portable Text, Contentful Rich Text, Lexical JSON, Markdown or clean HTML.

  6. 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.

Frequently asked questions

Can I just copy and paste rich text between CMSs?
For a handful of posts, yes, but pasting carries hidden formatting, inline styles and broken image links, and it doesn't scale. Past a few dozen posts, a script is faster and more consistent. It also lets you re-run the conversion after a content freeze, which manual pasting can't do.
Is Portable Text better than HTML for storing content?
Portable Text is better when content must render in several places, such as a website, an app and email, because it stores structure instead of presentation. HTML is simpler when content only ever renders on one website. The real advantage of structured formats is that custom blocks stay explicit instead of hiding inside markup.
How do I migrate rich text with embedded entries or references?
Migrate the referenced content first, keep a map from old IDs to new IDs, then convert the rich text and replace each old reference with the new one. This applies to Contentful embedded entries, Sanity references inside Portable Text and linked items in other CMSs. Converting rich text before its references exist leaves broken links.
Can AI convert rich text between CMS formats?
AI can help write the converter and handle odd one-off posts, but it shouldn't convert every post freehand. A language model may silently rewrite or drop content, which is hard to spot at scale. Use deterministic code for the conversion, and use AI to draft rules, classify edge cases and review flagged output.

· CEO & Design Director

Copywriter, designer and developer, and founder of Designify Studio. Over five years, 60+ projects across 14+ industries, for startups that have raised $300M+.

Want this done for you? Monthly retainer, $6,000.

Your ongoing design and development team, for products that keep shipping. $6,000, per month, flat fee. Book a free 30-minute call to see if it fits.

Schedule Call