There are two main approaches to convert your Lexical-based rich text to HTML:
Generate HTML on-demand (Recommended): Convert JSON to HTML wherever you need it, on-demand.
Generate HTML within your Collection: Create a new field that automatically converts your saved JSON content to HTML. This is not recommended because it adds overhead to the Payload API.
On-demand
To convert JSON to HTML on-demand, use the convertLexicalToHTML function from @payloadcms/richtext-lexical/html. Here's an example of how to use it in a React component in your frontend:
exportconstMyComponent=({ data }:{data:SerializedEditorState})=>{
9
const html =convertLexicalToHTML({ data })
10
11
return<divdangerouslySetInnerHTML={{__html: html }}/>
12
}
Dynamic Population (Advanced)
By default, convertLexicalToHTML expects fully populated data (e.g. uploads, links, etc.). If you need to dynamically fetch and populate those nodes, use the async variant, convertLexicalToHTMLAsync, from @payloadcms/richtext-lexical/html-async. You must provide a populate function:
exportconstMyComponent=({ data }:{data:SerializedEditorState})=>{
10
const[html, setHTML]=useState<null|string>(null)
11
useEffect(()=>{
12
asyncfunctionconvert(){
13
const html =awaitconvertLexicalToHTMLAsync({
14
data,
15
populate:getRestPopulateFn({
16
apiURL:`http://localhost:3000/api`,
17
}),
18
})
19
setHTML(html)
20
}
21
22
voidconvert()
23
},[data])
24
25
return html &&<divdangerouslySetInnerHTML={{__html: html }}/>
26
}
Using the REST populate function will send a separate request for each node. If you need to populate a large number of nodes, this may be slow. For improved performance on the server, you can use the getPayloadPopulateFn function:
return html &&<divdangerouslySetInnerHTML={{__html: html }}/>
29
}
HTML field
The lexicalHTMLField() helper converts JSON to HTML and saves it in a field that is updated every time you read it via an afterRead hook. It's generally not recommended, as it creates a column with duplicate content in another format.
Important: When converting HTML to Lexical, <img> tags are NOT automatically uploaded. This is intentional because we don't know which uploads-enabled collection to add them to, and that collection may have required fields that cannot be auto-filled. Images will be omitted from the conversion unless you provide the proper data attributes. See Converting HTML with Images for guidance on handling images.
1
import{
2
convertHTMLToLexical,
3
editorConfigFactory,
4
}from'@payloadcms/richtext-lexical'
5
// Make sure you have jsdom and @types/jsdom installed
6
import{JSDOM}from'jsdom'
7
8
const lexicalJSON =convertHTMLToLexical({
9
editorConfig:await editorConfigFactory.default({
10
config,// Your Payload Config
11
}),
12
html:'<p>text</p>',
13
JSDOM,// Pass in the JSDOM import; it's not bundled to keep package size small
14
})
Converting HTML with Images
When converting HTML to Lexical, <img> tags require special handling because Payload does not automatically upload images to prevent unintended database modifications. Here are three approaches to handle images during HTML-to-Lexical conversion:
Approach 1: Pre-upload Images and Use Data Attributes (Recommended)
The most reliable approach is to upload images to Payload first, then reference them in your HTML using special data attributes before conversion.
1
import{
2
convertHTMLToLexical,
3
editorConfigFactory,
4
}from'@payloadcms/richtext-lexical'
5
import{ getPayload }from'payload'
6
import{JSDOM}from'jsdom'
7
import config from'@payload-config'
8
9
const payload =awaitgetPayload({ config })
10
11
// Step 1: Upload the image to Payload
12
const uploadedMedia =await payload.create({
13
collection:'media',// Your upload collection slug
14
data:{
15
alt:'My image description',
16
},
17
filePath:'/path/to/local/image.jpg',// or file: fileData for file uploads
18
})
19
20
// Step 2: Construct HTML with the proper data attributes
// The lexicalJSON will now contain a proper upload node
Required data attributes:
data-lexical-upload-id: The ID of the uploaded document
data-lexical-upload-relation-to: The collection slug (e.g., 'media')
Approach 2: Parse and Upload Images Before Conversion
For bulk content migration or when dealing with external image URLs, parse the HTML first, upload images, then replace the image tags with proper attributes.
Approach 3: Construct Upload Nodes with buildEditorState
If you already have upload IDs and want to build the Lexical JSON structure, use the buildEditorState helper for a type-safe, simplified approach. This helper requires less boilerplate and eliminates the need to manually construct the root node.
// Build Lexical JSON with upload nodes using the helper
5
const lexicalJSON =buildEditorState({
6
text:'Some text content',
7
nodes:[
8
{
9
type:'upload',
10
format:'',
11
version:3,
12
relationTo:'media',
13
value:'your-upload-id-here',// ID of the uploaded document
14
fields:{},// Any additional fields configured for the upload feature
15
id:uuid(),// Unique ID for this node instance (not the upload document ID)
16
},
17
],
18
})
19
20
// Save to your collection
21
await payload.create({
22
collection:'pages',
23
data:{
24
title:'My Page',
25
content: lexicalJSON,
26
},
27
})
Troubleshooting
Images disappear during conversion:
Ensure images have both data-lexical-upload-id and data-lexical-upload-relation-to attributes
Verify the upload ID exists in your upload collection
Check that the relationTo value matches your upload collection slug
"Cannot read property 'id' of undefined" errors:
The upload document may not exist - verify the ID is correct
Ensure the upload collection is properly configured
Check that the referenced upload hasn't been deleted
Images work in the admin UI but not in conversion:
The admin UI handles pending uploads differently than server-side conversion
Server-side conversion requires existing upload IDs, not pending uploads
Always upload images before converting HTML
Tip: For large-scale content migrations, consider creating a migration script that processes HTML in batches, uploads images in parallel with rate limiting, and handles errors gracefully.