---
title: "Versionsdiffs für benutzerdefinierte Lexical-Blöcke in Payload CMS"
description: "Dieser Artikel zeigt, wie sich aussagekräftige Versionsdiffs für benutzerdefinierte Lexical-Blöcke in Payload CMS rendern lassen."
---

![Porträt eines jungen Mannes mit kurzer brauner Frisur und blauem Hemd vor einer grünen, bewachsenen Landschaft.](https://jhb.software/media/jens-becker-768x768.jpg)

Geschrieben von

[Jens Becker](/de/autoren/jens-becker)

Veröffentlicht am

9\. Juli 2026

Zuletzt aktualisiert am

7\. September 2026

Tags

Payload CMS

Enthält ein Lexical-Rich-Text-Feld benutzerdefinierte Blöcke, zeigt Payloads Version-Diff-Ansicht diese nur als Slug — `my-block-slug` statt der geänderten Feldwerte:

[![Screenshot eines digitalen Vergleichs von Textversionen mit einem hervorgehobenen Unterschied im linken Abschnitt, der in rotem Hintergrund und blauer Markierung angezeigt wird.](https://jhb.software/media/payload-richtext-no-custom-diff-component-1024x310.png)](https://jhb.software/media/payload-richtext-no-custom-diff-component-2560x775.png)

Das macht den Versionsvergleich für jeden Block mit mehr als einem relevanten Feld unbrauchbar. Die Lösung ist eine benutzerdefinierte `DiffComponent`: eine React Server Component, die Payload beim Vergleich zweier Versionen eines Rich-Text-Felds rendert.

Der Ansatz besteht aus drei Teilen, die in den folgenden Abschnitten der Reihe nach behandelt werden. Zunächst wird eine benutzerdefinierte `DiffComponent` registriert, damit Payload unseren Code für das Feld aufruft. Dann werden im Inneren dieser Component beide Versionen des Inhalts in HTML konvertiert und die zwei Strings an Payloads eigenes Diff-Utility übergeben. Schließlich wird dieses HTML automatisch aus der Field-Config jedes Blocks generiert, sodass neue Blöcke keinen zusätzlichen Aufwand erfordern.

Das Ergebnis sieht so aus:

[![Vergleichsansicht eines Zwei-Spalten-Textblocks mit markierten Unterschieden; linke Seite zeigt eine frühere Version des Texts mit durchgestrichenen und roten Markierungen, rechte Seite die aktuelle Version mit hervorgehobenen Änderungen in Blau.](https://jhb.software/media/payload-richtext-custom-diff-component-1-1024x544.png)](https://jhb.software/media/payload-richtext-custom-diff-component-1-2560x1359.png)

## Eine eigene DiffComponent

Payload löst Admin-Komponenten über eine statische Import-Map auf, nicht über direkte Imports. Es wird keine Component-Instanz übergeben — stattdessen ein Pfad-String (wie `/shared/lexical/LexicalBlocksDiffComponent#LexicalBlocksDiffComponent`), den Payload in einer generierten `importMap.js` nachschlägt. Es gibt zwei Probleme: dem Editor mitzuteilen, welche Diff-Komponente zu verwenden ist, und sicherzustellen, dass diese Komponente in der Import-Map registriert ist, damit Payload sie zur Laufzeit finden kann.

## Die DiffComponent des Editors überschreiben

Wir haben `lexicalEditor()` in einem kleinen Helper `lexicalEditorWithBlockDiff` gekapselt, der beides an einer Stelle erledigt:

ts

```
/**
 * Wraps `lexicalEditor()` and replaces the default `DiffComponent` with a
 * project-provided one, registering it in the generated import map.
 */
export const lexicalEditorWithBlockDiff = (
  args: LexicalEditorArgs | undefined,
  { diffComponentPath }: Options,
) => {
  const base = lexicalEditor(args)
  return async (ctx: Parameters<ReturnType<typeof lexicalEditor>>[0]) => {
    const result = await base(ctx)
    const baseGenerateImportMap = result.generateImportMap
    return {
      ...result,
      DiffComponent: diffComponentPath,
      generateImportMap: (
        importMapArgs: Parameters<NonNullable<typeof baseGenerateImportMap>>[0],
      ) => {
        baseGenerateImportMap?.(importMapArgs)
        importMapArgs.addToImportMap(diffComponentPath)
      },
    }
  }
}
```

Zwei Dinge passieren hier:

-   `**DiffComponent**` — mit unserem eigenen Pfad-String überschrieben.
-   `**generateImportMap**` — erweitert, sodass unsere Komponente neben Payloads Defaults in die Import-Map aufgenommen wird. Ohne das kann Payload den Pfad nicht auflösen und die Diff-Ansicht bricht.

Der Wrapper ist für alles andere transparent, sodass er `lexicalEditor(...)` überall ersetzen kann: im Root-Editor in `payload.config.ts` und in jedem verschachtelten Rich-Text-Feld innerhalb eines Blocks, der einen eigenen Editor definiert.

In der Config ist er ein Drop-in-Ersatz für `lexicalEditor()`. `diffComponentPath` ist derselbe statische Import-Map-Pfad, den der Wrapper registriert:

ts

```
editor: lexicalEditorWithBlockDiff(
  {
    features: ({ defaultFeatures }) => [
      ...defaultFeatures,
      BlocksFeature({ blocks: [/* your blocks */] }),
    ],
  },
  { diffComponentPath: '/shared/lexical/LexicalBlocksDiffComponent#LexicalBlocksDiffComponent' },
)
```

Der Pfad verweist auf die Diff-Komponente, die die eigentliche Arbeit erledigt — wir bauen sie im nächsten Abschnitt. Nach der Einbindung muss die Import-Map neu generiert werden (`payload generate:importmap`), damit Payload den Pfad auflösen kann.

## Blöcke als HTML rendern und das Ergebnis vergleichen

Payload liefert `getHTMLDiffComponents` mit, ein Utility, das `fromHTML`\- und `toHTML`\-Strings entgegennimmt und highlighted Before/After-React-Trees erzeugt. Der Ansatz: Jede Version des Lexical-State wird in HTML konvertiert, dann übernimmt Payload das Diff — keine eigene Diff-Logik nötig.

`createLexicalBlocksDiffComponent` gibt eine `RichTextFieldDiffServerComponent` zurück, die:

1.  Mit `getPayloadPopulateFn` eine Populate-Funktion erstellt, damit Relationships und Uploads innerhalb von Blöcken zu echten Daten aufgelöst werden können.
2.  Die Vorher- (`comparisonValue`) und Nachher-Version (`versionValue`) des Lexical-State mit `convertLexicalToHTMLAsync` in HTML konvertiert und dabei eine Converter-Map verwendet, die benutzerdefinierte Blöcke rendern kann.
3.  Beide Strings an `getHTMLDiffComponents` übergibt.
4.  Das Ergebnis in Payloads `FieldDiffContainer` einbettet, damit es nativ aussieht.

Payload ruft die Component mit dem Request (`req`) und den zwei zu vergleichenden Feldwerten auf. `converters` ist die im nächsten Abschnitt zusammengestellte Block-Map — ein Converter pro registriertem Block, plus optionale Overrides. Der Kern ist kurz:

ts

```
// Resolves related documents (relationships, uploads) up to one level deep.
const populate = await getPayloadPopulateFn({ currentDepth: 0, depth: 1, req })

const fromHTML = await convertLexicalToHTMLAsync({ converters, data: valueFrom, populate }) // valueFrom = comparisonValue
const toHTML   = await convertLexicalToHTMLAsync({ converters, data: valueTo,   populate }) // valueTo   = versionValue

const { From, To } = getHTMLDiffComponents({
  fromHTML: fromHTML?.length ? fromHTML : '<p></p>',
  toHTML:   toHTML?.length   ? toHTML   : '<p></p>',
})
```

`populate` an `convertLexicalToHTMLAsync` zu übergeben ermöglicht es den Convertern, Relationships und Uploads innerhalb von Blöcken aufzulösen, sodass im Diff "Author: Jane Doe" oder ein echtes Bild-Thumbnail statt einer rohen Dokument-ID erscheint. `depth: 1` hält die Population flach: genug, um einen Titel oder ein Thumbnail zu rendern, ohne den gesamten Relationship-Graphen zu laden.

`createLexicalBlocksDiffComponent` ist eine Factory. Die Datei, auf die der Import-Map-Pfad verweist, tut nichts anderes als sie aufzurufen und das Ergebnis zu exportieren — das ist die Komponente, die Payload für die Diff-Ansicht lädt:  

ts

```
// LexicalBlocksDiffComponent.tsx — the file the import-map path resolves to
export const LexicalBlocksDiffComponent = createLexicalBlocksDiffComponent({
  overrides: blockOverrides, // optional; see below
})
```

## Automatisch generierte Converter

Für jeden Block von Hand einen HTML-Converter zu schreiben, ist nur eine andere Variante des ursprünglichen Problems. Stattdessen geht die Komponente die Payload-Field-Config jedes Blocks durch und generiert automatisch einen Converter.

Zur Renderzeit liest sie `req.payload.config.blocks` und baut aus den Field-Definitionen einen Converter für jeden registrierten Block:

ts

```
// allBlocks = req.payload.config.blocks; blocksMap is a slug → config lookup
const autoConverters: BlockDiffConvertersMap = {}
for (const block of allBlocks) {
  autoConverters[block.slug] = createAutoBlockConverter(block, blocksMap, { locale })
}
const blocks = { ...autoConverters, ...overrides }

// this is the `converters` passed to convertLexicalToHTMLAsync above
const converters = ({ defaultConverters }) => ({
  ...defaultConverters,
  blocks: { ...defaultConverters.blocks, ...blocks },
})
```

`createAutoBlockConverter` geht die Felder des Blocks rekursiv durch und rendert jeden mit einem typgerechten Helper — Scalar-Felder werden zu Label-Wert-Zeilen, Relationships und Uploads werden zu Titel oder Thumbnail aufgelöst, und verschachteltes `richText` wird durch dieselbe Converter-Map verarbeitet, sodass Blöcke in Blöcken einfach funktionieren. Strukturelle Wrapper (Rows, Tabs) werden geflacht; versteckte und rein UI-seitige Felder werden übersprungen.

Das Ergebnis: Ein neuer Block im Lexical-Editor erscheint automatisch mit einem lesbaren, feldweisen Layout im Versionsdiff — ohne zusätzlichen Code.

## Escape Hatches für benutzerdefinierte Layouts

Die automatische Generierung deckt den Standardfall ab, aber manche Blöcke benötigen ein Layout, das der Walker nicht ableiten kann — Side-by-Side-Spalten, benutzerdefinierte Gruppierungen und Ähnliches. Ein `overrides`\-Map, der über die automatisch generierten Converter gelegt wird, ermöglicht genau das:

ts

```
const TwoColumnRichTextOverride: BlockDiffConverter<TwoColumnRichTextBlock> = async (args) => {
  const { firstContent, secondContent } = args.node.fields
  const [first, second] = await Promise.all([
    richText(args, firstContent),
    richText(args, secondContent),
  ])
  return blockContainer(
    'Two-Column Text',
    `<div style="${styles.columns}">
       <div style="${styles.column}">${first}</div>
       <div style="${styles.column}">${second}</div>
     </div>`,
  )
}

export const blockOverrides = defineBlockConverters({
  twoColumnRichText: TwoColumnRichTextOverride,
})
```

Ein kleines Kit von Hilfsfunktionen — `blockContainer`, `field`, `richText`, `uploadArray` und eine `styles`\-Map, die auf Payloads Admin-CSS-Variablen wie `--theme-elevation-*` verweist — hält Overrides kurz und lässt sie nahtlos ins Light- und Dark-Theme des Admin-UI einfügen.

Der vollständige Code ist unter [github.com/jhb-dev/payload-lexical-block-diff](https://github.com/jhb-dev/payload-lexical-block-diff) verfügbar.

## Fazit

Eine benutzerdefinierte `DiffComponent` einzubinden und sie mit automatisch generierten Field-Convertern zu verbinden, verwandelt Payloads Versionsdiff von einer Liste aus Block-Slugs in einen feldweisen Vergleich, der tatsächlich nützlich ist. Neue Blöcke erhalten automatisch lesbare Diffs; die wenigen, die ein benutzerdefiniertes Layout benötigen, können genau das überschreiben. Die vollständige Implementierung ist im verlinkten Repository zu finden.