5.4 KiB
| title | aliases | tags | sources | created | updated | |||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Rich Text — Converting HTML (Lexical) |
|
|
|
2026-05-15 | 2026-05-15 |
Overview
Payload CMS Lexical rich text stores content as JSON (SerializedEditorState). Converting to/from HTML requires explicit functions — nothing happens automatically.
Rich Text → HTML
On-Demand (Recommended)
Use convertLexicalToHTML from @payloadcms/richtext-lexical/html. Synchronous, expects fully-populated data.
import { convertLexicalToHTML } from '@payloadcms/richtext-lexical/html'
const html = convertLexicalToHTML({ data })
On-Demand with Dynamic Population (Advanced)
If data contains un-populated nodes (uploads, links), use the async variant with a populate function:
import { convertLexicalToHTMLAsync } from '@payloadcms/richtext-lexical/html-async'
import { getRestPopulateFn } from '@payloadcms/richtext-lexical/client'
// Client-side (REST): one request per node — slow for many nodes
const html = await convertLexicalToHTMLAsync({
data,
populate: getRestPopulateFn({ apiURL: `http://localhost:3000/api` }),
})
// Server-side RSC (recommended for performance)
import { getPayloadPopulateFn } from '@payloadcms/richtext-lexical'
const html = await convertLexicalToHTMLAsync({
data,
populate: await getPayloadPopulateFn({ currentDepth: 0, depth: 1, payload }),
})
HTML Field (Not Recommended)
lexicalHTMLField() saves converted HTML into a dedicated field via afterRead hook. Creates duplicate content column — avoid unless needed for external consumers.
lexicalHTMLField({
htmlFieldName: 'content_html',
lexicalFieldName: 'content',
})
Custom Block Converters
When rich text includes wiki/payloadcms/rich-text-blocks, provide per-slug converters:
const htmlConverters: HTMLConvertersFunction<NodeTypes> = ({ defaultConverters }) => ({
...defaultConverters,
blocks: {
myTextBlock: ({ node, providedCSSString }) =>
`<div style="background-color: red;${providedCSSString}">${node.fields.text}</div>`,
},
inlineBlocks: {
myInlineBlock: ({ node, providedStyleTag }) =>
`<span${providedStyleTag}>${node.fields.text}</span>`,
},
})
HTML → Rich Text
Use convertHTMLToLexical from @payloadcms/richtext-lexical. Requires jsdom installed separately (not bundled).
import { convertHTMLToLexical, editorConfigFactory } from '@payloadcms/richtext-lexical'
import { JSDOM } from 'jsdom'
const lexicalJSON = convertHTMLToLexical({
editorConfig: await editorConfigFactory.default({ config }),
html: '<p>text</p>',
JSDOM,
})
Warning:
<img>tags are NOT automatically uploaded during HTML→Lexical conversion. Images are omitted unless you provide the correct data attributes.
Converting HTML with Images
Approach 1: Pre-upload + Data Attributes (Recommended)
- Upload image to Payload first
- Add
data-lexical-upload-idanddata-lexical-upload-relation-toattributes to<img> - Run
convertHTMLToLexical
<img
src="/media/image.jpg"
data-lexical-upload-id="abc123"
data-lexical-upload-relation-to="media"
/>
Approach 2: Parse → Upload → Convert (Bulk Migration)
Parse HTML with JSDOM, upload each image, update DOM attributes, then convert. Suitable for content migrations.
Approach 3: buildEditorState Helper
When you already have upload IDs, build Lexical JSON directly without HTML parsing:
import { buildEditorState } from '@payloadcms/richtext-lexical'
const lexicalJSON = buildEditorState({
text: 'Some text content',
nodes: [
{
type: 'upload',
format: '',
version: 3,
relationTo: 'media',
value: 'your-upload-id-here',
fields: {},
id: uuid(),
},
],
})
Key Takeaways
- Prefer on-demand conversion (
convertLexicalToHTML) overlexicalHTMLField— no duplicate DB column convertLexicalToHTMLis sync and requires pre-populated data; useconvertLexicalToHTMLAsyncwith a populate function if nodes are un-populated- Server-side: use
getPayloadPopulateFn(batched); client-side:getRestPopulateFn(1 request/node) <img>tags are silently dropped during HTML→Lexical unlessdata-lexical-upload-idanddata-lexical-upload-relation-toattributes are presentjsdommust be installed manually — it is not bundled with@payloadcms/richtext-lexical- Custom blocks need custom converters — each block slug must map to a converter function
buildEditorStateis the cleanest approach when you already have upload IDs and want to skip HTML parsing entirely
Related
- wiki/payloadcms/rich-text-converters — editorConfigFactory, JSX/Markdown/Plaintext converters
- wiki/payloadcms/rich-text-blocks — block data structure, rendering
- wiki/payloadcms/fields-rich-text — field config, Lexical editor setup
- wiki/payloadcms/upload — upload collection config, storage adapters
- wiki/payloadcms/local-api —
payload.createfor uploading media during migration
Sources
raw/rich-text__converting-html.md- https://payloadcms.com/docs/rich-text/converting-html