# PayloadCMS Lexical Rich Text Format

PayloadCMS uses the Lexical editor by default. The publishing script converts markdown to Lexical JSON automatically via `convertMarkdownToLexical()` in `payloadcms-utils.js`, which uses a headless Lexical editor with PayloadCMS's own node classes and markdown transformers from `@payloadcms/richtext-lexical`.

## Root structure

```json
{
  "root": {
    "type": "root",
    "format": "",
    "indent": 0,
    "version": 1,
    "children": [ ... ],
    "direction": null
  }
}
```

## Supported node types

| Markdown | Lexical Node | Key Properties |
|----------|-------------|----------------|
| `# Heading` | `heading` | `tag: "h1"` through `"h6"` |
| Paragraph text | `paragraph` | `children: [text nodes]`, `textFormat`, `textStyle` |
| `- list item` | `list` + `listitem` | `listType: "bullet"` or `"number"`, `tag: "ul"` or `"ol"` |
| `> blockquote` | `quote` | Wraps text children directly |
| `---` | `horizontalrule` | DecoratorNode, no children |
| `[link](url)` | `link` | `fields: { url, linkType, newTab }`, `version: 3` |
| `\| col \| col \|` | `table` > `tablerow` > `tablecell` | `headerState: 0\|1`, `colSpan`, `rowSpan` |
| `![alt](path)` | `upload` | `relationTo: "media"`, `value: "<media-id>"`, `id: "<node-id>"`, `fields: {}`, `version: 3` |
| `` ```lang `` | `block` | `fields: { blockType: "Code", code, language, id }`, `version: 2` |
| `- [ ] item` | `list` + `listitem` | `listType: "check"`, `checked: true\|false` |

Code blocks (triple backticks) are converted to PayloadCMS `block` nodes with `blockType: "Code"` via the premade CodeBlock feature. The `language` field captures the language tag and `code` contains the block content. The PayloadCMS instance must have `BlocksFeature({ blocks: [CodeBlock()] })` enabled to render these blocks.

Checklists (`- [ ]` / `- [x]`) produce `list` nodes with `listType: "check"`. Each `listitem` has a `checked` boolean. The PayloadCMS instance must have `ChecklistFeature()` enabled.

Images (`![alt](path)`) are handled during `publishContent()` — the file is uploaded to the `media` collection first, then an `upload` node referencing the media ID is injected into the Lexical JSON. Upload nodes require both an `id` (unique node identifier, not the media document ID) and a `fields` object (can be empty `{}`). Without these, the PayloadCMS frontend renderer fails to display the image in preview. Tables require the `EXPERIMENTAL_TableFeature` to be enabled on the PayloadCMS instance.

## Text formatting (bitwise flags)

Inline formatting uses Lexical's bitwise format codes:

| Format | Code | Markdown |
|--------|------|----------|
| Plain | `0` | `text` |
| Bold | `1` | `**text**` |
| Italic | `2` | `*text*` |
| Bold + Italic | `3` | `***text***` |
| Strikethrough | `4` | `~~text~~` |
| Underline | `8` | N/A |
| Code | `16` | `` `text` `` |
| Subscript | `32` | N/A |
| Superscript | `64` | N/A |
| Highlight | `128` | `==text==` |

Formats combine via bitwise OR. For example, bold italic code = `1 | 2 | 16 = 19`.

## Text node structure

```json
{
  "type": "text",
  "text": "Hello world",
  "format": 0,
  "mode": "normal",
  "style": "",
  "detail": 0,
  "version": 1
}
```

## Coverage: markdown-producible vs non-markdown nodes

The converter handles **every Lexical node type that can be produced from markdown input**. The table above is the complete set.

PayloadCMS's default Lexical editor also registers these node types, which are **not producible from markdown** and therefore not part of this conversion pipeline:

| Node | Purpose | How to use |
|------|---------|------------|
| `relationship` | Embed a reference to another collection document | Programmatic only — use `RelationshipServerNode` |
| `autolink` | Auto-detected URLs in editor typing | Client-side only — never produced from markdown |

These nodes exist for interactive editor use and API round-tripping, not markdown conversion.

### What about hero images, SEO, excerpts?

These are **collection-level fields**, not Lexical editor nodes. They're set via frontmatter and passed through to the PayloadCMS API payload. See [field-mapping.md](field-mapping.md) for details.

### Inline images

PayloadCMS Lexical does not support inline images. `UploadNode` is block-level only. Images are always rendered as standalone blocks between paragraphs.

## Validation

The converter uses Lexical's own serialization (`editor.getEditorState().toJSON()`), so the output is guaranteed to be structurally valid. No separate validation step is needed — the JSON is identical to what PayloadCMS's editor produces.
