Zum Hauptinhalt springen
tsecurity.de LIVE
Echtzeit-Radar & Feeds
Alle RSS Feeds
👥 Community & Social
Sichere ProgrammierungI wanted the diff, not a screenshot: a small URL-change API(24.09.2026 um 06:05 Uhr)
Sichere ProgrammierungFreeze Object Identity Before One Mutator Extract(24.09.2026 um 06:06 Uhr)
Sichere ProgrammierungRun an n8n workflow when a page's text changes(24.09.2026 um 06:12 Uhr)
Sichere ProgrammierungThe Spreadsheet That Runs Your Company (And Why That Should Worry You)(24.09.2026 um 06:12 Uhr)
Sichere ProgrammierungArchitecting an Enterprise Network on AWS Cloud WAN(24.09.2026 um 06:31 Uhr)
Sichere ProgrammierungI wanted the diff, not a screenshot: a small URL-change API(24.09.2026 um 06:05 Uhr)
Sichere ProgrammierungFreeze Object Identity Before One Mutator Extract(24.09.2026 um 06:06 Uhr)
Sichere ProgrammierungRun an n8n workflow when a page's text changes(24.09.2026 um 06:12 Uhr)
Sichere ProgrammierungThe Spreadsheet That Runs Your Company (And Why That Should Worry You)(24.09.2026 um 06:12 Uhr)
Sichere ProgrammierungArchitecting an Enterprise Network on AWS Cloud WAN(24.09.2026 um 06:31 Uhr)
Intelligence View
⚡ tsecurity.de Intelligence

Building a custom Contentful Rich Text editor: the round-trip problem

Contentful lets you replace the UI of a Rich Text field with your own App. That sounds like a front-end job — build a nicer editor, mount it in the field location, done. It isn't. The editor UI is the easy 20%. The hard 80% is a small f…

0
↗ Quelle (dev.to)
Reagiere als Erste:r — dein Feedback zählt!

Contentful lets you replace the UI of a Rich Text field with your own App. That sounds like a front-end job — build a nicer editor, mount it in the field location, done.



It isn't. The editor UI is the easy 20%. The hard 80% is a small file most people never think about: the layer that converts between your editor's internal model and Contentful's stored document — losslessly, in both directions.



I built one on top of PlateJS (which is a Slate editor underneath), and this post is about that conversion layer — the part that actually decides whether the whole thing works.






Two models that don't match



Contentful Rich Text is a fixed JSON schema (@contentful/rich-text-types): a document whose content is a tree of blocks, inlines and text nodes, where formatting lives on text nodes as an array of marks:




{
"nodeType": "document",
"content": [
{
"nodeType": "paragraph",
"content": [
{ "nodeType": "text", "value": "Hello ", "marks": [] },
{ "nodeType": "text", "value": "world", "marks": [{ "type": "bold" }] }
]
}
]
}






Slate/Plate uses a different shape for the same idea: { type, children } elements, and text nodes where marks are just boolean props:




[
{
type: "p",
children: [
{ text: "Hello " },
{ text: "world", bold: true }
]
}
]






Same document, two representations. The editor speaks Plate; the field stores Contentful. So you need two functions:




Contentful document ──deserialize()──▶ Plate value ──▶ editor
field ◀──serialize()──── Plate value ◀── (debounced onChange)






And they have to be exact inverses. If serialize(deserialize(doc)) isn't equal to doc, then simply opening an entry and saving it — without touching anything — silently corrupts content. That's the round-trip problem.






The design decision that made it tractable



The temptation is to write a clever generic converter. I did the opposite: I made the two models mirror each other on purpose, so the transform is a thin, explicit mapping instead of a guessing game.



Plate node types are named to shadow Contentful's node types — p, h1, ul, li, blockquote, embedded-entry-block — so the mapping is just two lookup tables that are literal inverses of each other:




const BLOCK_TO_CF: Record<string, string> = {
p: BLOCKS.PARAGRAPH,
h1: BLOCKS.HEADING_1,
h2: BLOCKS.HEADING_2,
h3: BLOCKS.HEADING_3,
blockquote: BLOCKS.QUOTE,
hr: BLOCKS.HR,
ul: BLOCKS.UL_LIST,
ol: BLOCKS.OL_LIST,
li: BLOCKS.LIST_ITEM,
};

// the inverse falls out for free
const CF_TO_BLOCK = Object.fromEntries(
Object.entries(BLOCK_TO_CF).map(([k, v]) => [v, k]),
);






Marks get the same treatment — a bold mark on a Contentful text node becomes a bold: true prop on a Plate leaf, and back:




const MARK_PROPS = ["bold", "italic", "underline", "code"] as const;
const MARK_TO_CF: Record<(typeof MARK_PROPS)[number], string> = {
bold: MARKS.BOLD,
italic: MARKS.ITALIC,
underline: MARKS.UNDERLINE,
code: MARKS.CODE,
};
const CF_TO_MARK = Object.fromEntries(
Object.entries(MARK_TO_CF).map(([k, v]) => [v, k]),
);






Deriving each inverse table with Object.fromEntries instead of hand-writing it isn't just less typing — it makes the inverse structurally guaranteed. You can't forget to add the reverse mapping for a node type, because there's only one source of truth.






The edge cases are the whole game



A demo with a bold paragraph round-trips fine. Real content breaks on the corners. The ones that mattered:



Empty nodes. Slate requires every element to have at least one child. Contentful is fine with an empty block. So deserializing has to backfill:




const children = node.content.map(cfNodeToPlate).filter(Boolean);
const ensureChildren = children.length ? children : [{ text: "" }];






Skip this and Slate throws the moment it tries to render an empty paragraph.



Void nodes — embeds. An embedded entry or asset has no editable text; it's a card that resolves its title through the CMA. In Slate terms that's a void node, and voids still need a dummy text child to satisfy the schema:




case BLOCKS.EMBEDDED_ENTRY:
return {
type: "embedded-entry-block",
entryId: node.data?.target?.sys?.id, // carry the id, nothing else
children: [{ text: "" }], // required, even though it's void
};






The only thing you keep is the entry id. Everything visible about that card is resolved live — get the transform wrong here and you either lose the reference or store an invalid node the CDA rejects.



Unsupported node types. A generic editor doesn't implement every Contentful block. The safe move is to skip what you don't understand rather than crash or fabricate:




const plateType = CF_TO_BLOCK[node.nodeType];
if (!plateType) return null; // skip gracefully






Contentful-valid nesting. Lists have to be ul > li > p, not ul > li with raw text. If the editor produces the wrong nesting, the value looks fine in Plate and fails validation only when you save. The mapping has to produce schema-valid structure, not just structurally-similar structure.






Pin it with round-trip tests



None of this is provable by clicking around. The transform is a pure function with no React and no Contentful SDK in sight, which means it's trivially unit-testable — and the test that matters is the round-trip identity:




// for a representative document:
expect(serialize(deserialize(doc))).toEqual(doc);






Because the module is framework-free, the tests run in plain node --test without booting the editor. That's the payoff of keeping the conversion layer isolated: the riskiest part of the app is also the cheapest part to test exhaustively.






The takeaway



If you build a custom Contentful editor — or any editor that has to persist to a schema you don't control — treat the conversion layer as the core of the project, not an afterthought. Concretely:





  • Make the two models mirror each other so the mapping is explicit, not inferred.


  • Derive inverse tables from one source so you can't desync them.


  • Enumerate the edge cases — empty nodes, void nodes, unsupported types, valid nesting — because that's where round-trip breaks.


  • Isolate it from the framework and pin it with round-trip tests, since silent corruption is invisible until it isn't.
    The editor UI is what people see. The transform is what keeps their content intact.






The full editor is open source — code and a live demo. It's part of a set of Contentful/Next.js projects I keep public at vitaliipopov.dev.

SOC Incident Playbook: Remote Code Execution (RCE) Defense
title: Detect Exploitation - Building a custom Contentful Rich Text editor: the round-trip problem
id: 3f19abe5-87ce-4089-9c43-9b33edf4c359
status: experimental
description: Automatisch generierte SIEM-Erkennungsregel basierend auf CTI Intelligence
references:
  - https://tsecurity.de/
author: iShareStuff CTI Automated Detection Engine
date: 2026-09-24
logsource:
  category: network_connection
  product: any
detection:
  selection:
      CommandLine|contains:
        - 'exploit'
  condition: selection
falsepositives:
  - Legitime administrative Zugriffe oder Penetrationstests
level: high
tags:
  - attack.initial_access
rule CTI_Threat_Indicator {
    meta:
        author = "iShareStuff CTI Automated Detection Engine"
        date = "2026-09-24"
        description = "YARA Signature for "
    strings:
        $str = "Building a custom Contentful R" ascii wide
    condition:
        any of them
}
tsecurity.de Cognitive Threat RAG
Fokus-Vektor:

Kognitive Analyse für identifizierte Bedrohung: Erhöhte Bedrohungslage im Bereich Building a custom Contentful Rich Text e.... Basierend auf 368k Vektor-Korrelationen werden sofortige Isolationsmaßnahmen für betroffene Endpunkte empfohlen.

🛡️ Angriffsfläche & Exposure

Netzwerk/Remote-Zugriff ohne Vorauthentifizierung möglich.

Empfohlene Sofortmaßnahmen
  • 1. Perimeter-Inspektion: Relevante Portfreigaben und exponierte Endpunkte unverzüglich scannen.
  • 2. Patch-Applikation: Hersteller-Hotfix einspielen oder betroffene Daemons in isolierte DMZ-Segmente überführen.
  • 3. Telemetrie & EDR-Alerts: Prozessaufrufe und Child-Processes auf anomale Shell-Spawns überwachen.
🔗 Semantisch verwandte Zero-Days MariaDB 11.7 VEC
Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten Building a custom Contentful Rich Text editor: the round-trip problem

Thematisch verwandte Begriffe: Building, custom, Contentful, Rich · 6 Treffer

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Zum Aktualisieren ziehen
ZERO-DAY CVE-2026-96676 | A vulnerability was identified in Fast FAC1900R 20190827_2.0.2. The impa…
Advisory →
TTS Reader • tsecurity.de Voice
tsecurity.de Icon
tsecurity.de App
Offline-Lesen, Eilmeldungen & 0ms Ladezeit

Installiere tsecurity.de direkt auf deinen Home-Bildschirm für das ultimative Vollbild-Magazinerlebnis ohne Browser-Leisten.

Nächster Beitrag
Themen-Radar & Intelligence Matrix
Echtzeit-Taxonomie nach Angriffsvektoren & Plattformen

tsecurity.de Live Threat Radar

🔴 LIVE RADAR
MONITORING
AKTIV
CVE-DATENBANK
LIVE
🔍
Community Radar & Live Chat
Sentinel Bot online • Live-Stream
Dein Cluster: Security Explorer
Match:
lädt…
Verbindung zum Community-Stream wird aufgebaut...
Bearbeitungsmodus — Senden überschreibt deine Nachricht
Community-Puls — was gerade passiert
lädt…
Aktivitäten deiner Analysten
lädt…
Neues Thema oder Eilmeldung einreichen

Reiche interessante Links, Zero-Days oder Debatten ein. Die Community entscheidet per Upvote über die Veröffentlichung.

Heiß diskutierte Einreichungen
🔖 Gespeicherte Artikel
📂 Keine gespeicherten Artikel vorhanden.
Zurück Ziehen Vor
Links: vorheriger Artikel Rechts: nächster Artikel unten: schließen
News NIS-2 Frühwarnung Tier-1 Intel TTP ⏱️ 3 Min vor 10 Min
Artikeldaten werden geladen...

Zurück: vorheriger Vor: nächster
↗ Original-Quelle
Social Reaktionen Deine Reaktion zählt
Einstufung & Relevanz-Poll 0 Stimmen
In sozialen Netzwerken teilen 1-Klick