LLumen
← All articles

Slate ⇄ Contentful: building a lossless rich-text transform

Cover — slate-contentful-transform

Replacing Contentful's rich-text editor with a custom PlateJS one is mostly a UI project — until the moment you save. The editor thinks in Slate nodes; the CMS stores a strict JSON schema. The transform between them is where such projects quietly die.

Two trees, one meaning

Contentful Rich Text is a fixed vocabulary: paragraph, heading-1 through heading-6, lists that must nest as list > list-item > paragraph, text nodes carrying marks. Slate is freeform by design. The trick is to stop treating that freedom as a feature:

  • Name the Plate node types after the Contentful ones — the mapping becomes a lookup table.

  • Enforce Contentful's nesting rules inside the editor, so invalid trees can't exist to begin with.

  • Make unsupported nodes degrade explicitly instead of silently disappearing.

Round-trip or it didn't happen

The only serialisation test that matters is the round trip: serialize(deserialize(doc)) must reproduce the document byte for byte — headings, marks, hyperlinks, nested lists, embedded entries, all of it. That test caught every regression this editor has had, usually within seconds of writing the bug.

A rich-text editor without a round-trip test is a data-loss incident on a delay timer.


The transform is ~200 lines and completely boring, which is the highest compliment serialisation code can earn. The editor above it can now evolve freely: toolbar, slash menu, custom blocks — none of it can corrupt the stored document.