Building a rich-text renderer for a headless CMS seems like a straightforward task on paper. Contentful gives you a document tree. Sanity gives you Portable Text. You write a component that maps each node type to a React element, run it against a few test entries, everything renders, and you ship it.
Then an editor discovers what yours is missing. The embedded asset that renders nothing because you handled images in BLOCKS.EMBEDDED_ASSET but not in INLINES.EMBEDDED_ASSET. The table that throws because you forgot tableRow and tableCell in your node map. The ordered list inside an unordered list that your renderer flattens because you assumed lists do not nest. The bold italic link that loses its italic because your mark renderer applies transforms sequentially and the last one wins.
These are not obscure edge cases. They are the predictable output of editors who have been given a rich-text field and told to write.
The nodes developers routinely miss
Embedded assets in inline position
Contentful's rich-text field distinguishes between block-level embedded entries and assets, and inline-level ones. Most renderers handle BLOCKS.EMBEDDED_ASSET (a standalone image block) but miss INLINES.EMBEDDED_ASSET (an image embedded within a paragraph). Editors use inline assets for icons, logos, and inline figures — if your renderer does not handle the inline variant, those assets simply disappear without any visible error.
Horizontal rules
The hr element is available in most rich-text editors and is used more often than developers expect. It tends to be the last node type added to a renderer and the first one forgotten. An unhandled HR node in Contentful's document format renders nothing — no error, no fallback, just a silent gap where a section divider should be.
Tables
Table support was added to Contentful's rich-text field gradually and is absent in many older renderers. Sanity's Portable Text does not include a native table type — tables are typically handled via custom block schemas — but the same problem applies: if your renderer was built before tables were part of the schema, they are likely unhandled. Unlike missing hr nodes, a missing table renderer often throws rather than failing silently, which is how most teams discover the gap.
Code blocks
Contentful's rich-text includes a code mark for inline code and a separate code block type. Many renderers handle the inline mark but miss the block variant. The block variant is what editors use when they paste in multi-line code samples, and it is the one that benefits most from monospace rendering and overflow handling.
The mark combination problem
Rich-text marks — bold, italic, underline, code, link — can be applied in any combination, and the order in which they are applied is not guaranteed. An editor can select text and apply bold, then italic, then link. Another editor applies italic, then bold, then link, and ends up with the same visual result but a different mark order in the document structure.
Renderers that apply marks by wrapping elements sequentially can produce incorrect output when marks nest in unexpected orders. The most common failure is a link losing its underline or colour because the em or strong wrapper resets the cascade. Testing with a single bold word or a single link never surfaces this. It only appears when all three marks are applied to the same text, which editors do constantly.
The adjacent node problem
Rich-text documents can place any node type adjacent to any other node type. Two unordered lists in sequence — because an editor added a new list without realising they were not extending the existing one — are a common example. In HTML these render as two separate ul elements with whatever margin is set on the element. In your React renderer, they both call the same list component, and if that component has a margin-top rule, the gap between them is doubled.
Other common adjacency issues: a blockquote followed immediately by another blockquote (editors quote multiple sources), an image block followed immediately by a heading (no paragraph between them), and a code block at the very start of the document before any paragraph has appeared.
Using HTML as a spec
One practical approach to renderer testing is to use comprehensive placeholder HTML as the expected output and work backward from it. Generate a block of HTML that covers every element your schema supports — headings, lists, inline marks in combination, figures, blockquotes, tables, code blocks, horizontal rules — and treat it as the spec your renderer should produce.
Write your Contentful or Sanity content to reproduce that HTML structure, run it through your renderer, and compare the output. The gaps between the generated HTML and your renderer's actual output are exactly the node types and edge cases you have not handled yet. This is more reliable than trying to enumerate all possible node types from the documentation, because documentation tends to lag behind what editors can actually produce.
Rich-text renderers are one of those components that feel done long before they are. The only way to know what yours is missing is to give it the full range of content your editors can produce and see what breaks. Doing that before launch is considerably less stressful than doing it after.