Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Lexical default sync HTML converter for Upload Node not working #14310

Unanswered
4external asked this question in Q&A
Discussion options

Describe the Bug

Lexical default sync HTML converter for Upload Node not working.

Here is content for field content in MongoDB:

content: {
 "root": {
 "children": [
 {
 "children": [
 {
 "detail": 0,
 "format": 0,
 "mode": "normal",
 "style": "",
 "text": "Hello!",
 "type": "text",
 "version": 1
 }
 ],
 "direction": null,
 "format": "",
 "indent": 0,
 "type": "paragraph",
 "version": 1,
 "textFormat": 0,
 "textStyle": ""
 },
 {
 "type": "upload",
 "version": 3,
 "format": "",
 "id": "68f0b685904c7776adaf65ed",
 "fields": null,
 "relationTo": "media",
 "value": "68f0b665d86c759e1d14247a"
 },
 {
 "children": [
 {
 "detail": 0,
 "format": 0,
 "mode": "normal",
 "style": "",
 "text": "bye",
 "type": "text",
 "version": 1
 }
 ],
 "direction": null,
 "format": "",
 "indent": 0,
 "type": "paragraph",
 "version": 1,
 "textFormat": 0,
 "textStyle": ""
 }
 ],
 "direction": null,
 "format": "",
 "indent": 0,
 "type": "root",
 "version": 1
 }
}

Simple code for collection

 hooks: {
 beforeChange: [
 async ({ data }) => {
 const html = convertLexicalToHTML({ data: data.content })
 console.log(html)
 return data
 },
 ],
 },

return formatted text without upload node content
<div class="payload-richtext"><p>Hello!</p><p>bye</p></div>

Debuging show that converter wait for node as Object
if (typeof uploadNode.value !== 'object') { return '' } in

but uploadNode.value = "68f0b665d86c759e1d14247a" and typeof uploadNode.value = 'string'

Link to the code that reproduces this issue

pnpx create-payload-app@latest -t blank

Reproduction Steps

  1. create blank project
  2. add simple Pages collection and to config
import type { CollectionConfig } from 'payload'
import { convertLexicalToHTML } from '@payloadcms/richtext-lexical/html'
export const Pages: CollectionConfig = {
 slug: 'pages',
 fields: [
 {
 name: 'content',
 type: 'richText',
 },
 ],
 hooks: {
 beforeChange: [
 async ({ data }) => {
 const html = convertLexicalToHTML({ data: data.content })
 console.log(html)
 return data
 },
 ],
 },
}
  1. upload image
  2. add page to Pages, add upload to rich text content
  3. convert rich text with sync default HTML converters

Which area(s) are affected? (Select all that apply)

plugin: richtext-lexical

Environment Info

Binaries:
 Node: 23.2.0
 npm: 10.9.0
 Yarn: 1.22.22
 pnpm: 10.17.1
Relevant Packages:
 payload: 3.59.1
 next: 15.4.4
 @payloadcms/db-mongodb: 3.59.1
 @payloadcms/email-nodemailer: 3.59.1
 @payloadcms/graphql: 3.59.1
 @payloadcms/next/utilities: 3.59.1
 @payloadcms/payload-cloud: 3.59.1
 @payloadcms/richtext-lexical: 3.59.1
 @payloadcms/translations: 3.59.1
 @payloadcms/ui/shared: 3.59.1
 react: 19.1.0
 react-dom: 19.1.0
Operating System:
 Platform: darwin
 Arch: arm64
 Version: Darwin Kernel Version 25.0.0: Wed Sep 17 21:41:39 PDT 2025; root:xnu-12377.1.9~141/RELEASE_ARM64_T8103
 Available memory (MB): 8192
 Available CPU cores: 8
You must be logged in to vote

Replies: 4 comments 1 reply

Comment options

Please add a reproduction in order for us to be able to investigate.

Depending on the quality of reproduction steps, this issue may be closed if no reproduction is provided.

Why was this issue marked with the invalid-reproduction label?

To be able to investigate, we need access to a reproduction to identify what triggered the issue. We prefer a link to a public GitHub repository created with create-payload-app@latest -t blank or a forked/branched version of this repository with tests added (more info in the reproduction-guide).

To make sure the issue is resolved as quickly as possible, please make sure that the reproduction is as minimal as possible. This means that you should remove unnecessary code, files, and dependencies that do not contribute to the issue. Ensure your reproduction does not depend on secrets, 3rd party registries, private dependencies, or any other data that cannot be made public. Avoid a reproduction including a whole monorepo (unless relevant to the issue). The easier it is to reproduce the issue, the quicker we can help.

Please test your reproduction against the latest version of Payload to make sure your issue has not already been fixed.

I added a link, why was it still marked?

Ensure the link is pointing to a codebase that is accessible (e.g. not a private repository). "example.com", "n/a", "will add later", etc. are not acceptable links -- we need to see a public codebase. See the above section for accepted links.

Useful Resources

You must be logged in to vote
0 replies
Comment options

I believe I've just hit the same issue

Error converting lexical node to HTML: TypeError: Cannot read properties of undefined (reading 'startsWith')
node: {
 id: '68f5dcd3fa95823a6989a5c6',
 type: 'upload',
 value: {
 id: '9e085dc7-db63-4e32-86ef-74c7bb1df81d',
 alt: 'Sun station',
 url: '/api/media/file/Sun_station.webp',
 filename: 'Sun_station.webp'
 },
 fields: null,
 format: '',
 version: 3,
 relationTo: 'media'
}

... with the following editor state:

{
 "root": {
 "type": "root",
 "format": "",
 "indent": 0,
 "version": 1,
 "children": [
 {
 "tag": "h1",
 "type": "heading",
 "format": "",
 "indent": 0,
 "version": 1,
 "children": [
 {
 "mode": "normal",
 "text": "Hello from team!",
 "type": "text",
 "style": "",
 "detail": 0,
 "format": 1,
 "version": 1
 }
 ],
 "direction": null,
 "textFormat": 1
 },
 {
 "type": "paragraph",
 "format": "",
 "indent": 0,
 "version": 1,
 "children": [
 {
 "mode": "normal",
 "text": "Nice to see you",
 "type": "text",
 "style": "",
 "detail": 0,
 "format": 0,
 "version": 1
 }
 ],
 "direction": null,
 "textStyle": "",
 "textFormat": 0
 },
 {
 "type": "paragraph",
 "format": "",
 "indent": 0,
 "version": 1,
 "children": [
 {
 "mode": "normal",
 "text": "Here are the reasons:",
 "type": "text",
 "style": "",
 "detail": 0,
 "format": 0,
 "version": 1
 }
 ],
 "direction": null,
 "textStyle": "",
 "textFormat": 0
 },
 {
 "tag": "ol",
 "type": "list",
 "start": 1,
 "format": "",
 "indent": 0,
 "version": 1,
 "children": [
 {
 "type": "listitem",
 "value": 1,
 "format": "",
 "indent": 0,
 "version": 1,
 "children": [
 {
 "mode": "normal",
 "text": "Blah blah.",
 "type": "text",
 "style": "",
 "detail": 0,
 "format": 0,
 "version": 1
 }
 ],
 "direction": null
 },
 {
 "type": "listitem",
 "value": 2,
 "format": "",
 "indent": 0,
 "version": 1,
 "children": [
 {
 "mode": "normal",
 "text": "Sun station",
 "type": "text",
 "style": "",
 "detail": 0,
 "format": 0,
 "version": 1
 }
 ],
 "direction": null
 }
 ],
 "listType": "number",
 "direction": null
 },
 {
 "id": "68f5dcd3fa95823a6989a5c6",
 "type": "upload",
 "value": "9e085dc7-db63-4e32-86ef-74c7bb1df81d",
 "fields": null,
 "format": "",
 "version": 3,
 "relationTo": "media"
 },
 {
 "type": "quote",
 "format": "",
 "indent": 0,
 "version": 1,
 "children": [
 {
 "mode": "normal",
 "text": "The past is past, now, but that’s... you know, that’s okay! It’s never really gone completely. The future is always built on the past, even if we won’t get to see it.",
 "type": "text",
 "style": "",
 "detail": 0,
 "format": 0,
 "version": 1
 }
 ],
 "direction": null
 }
 ],
 "direction": null
 }
}

... using the officially recommended way of conversion:

 const payload = await getPayload({ config: payloadConfig })
 const html = await convertLexicalToHTMLAsync({
 data,
 populate: await getPayloadPopulateFn({
 payload,
 depth,
 currentDepth: 0,
 }),
 })
You must be logged in to vote
0 replies
Comment options

I've spent some time reading the codebase and from what I understand, the relationship field stored in Lexical editor state gets populated with partial data when editor state gets selected prior, so the actual state stored in the database...

 {
 "id": "68f5dcd3fa95823a6989a5c6",
 "type": "upload",
 "value": "9e085dc7-db63-4e32-86ef-74c7bb1df81d", // <--- Only ID here.
 "fields": null,
 "format": "",
 "version": 3,
 "relationTo": "media"
 },

... somehow turns into

{
 id: '68f5dcd3fa95823a6989a5c6',
 type: 'upload',
 value: { // <--- Partially populated data.
 id: '9e085dc7-db63-4e32-86ef-74c7bb1df81d',
 alt: null,
 url: '/api/media/file/Sun_station-1.webp',
 filename: 'Sun_station-1.webp'
 },
 fields: null,
 format: '',
 version: 3,
 relationTo: 'media'
}

... so the converter assumes all the necessary data is present and fails to call .mimeType.startsWith() below:

I assume this can be worked around by not populating the relationship field which is suboptimal.

It's quite unexpected to me that Payload somehow inspects the Lexical editor state for relationships and populates them beforehand.

You must be logged in to vote
0 replies
Comment options

I converted this into a discussion, as this is expected behavior - check out this section of the docs.

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

Lexical upload/relationship/internal link nodes behave the exact same way as payload upload/relationship fields - when you fetch the data with a depth > 0, the related document value will automatically be populated.

The synchronous html converter requires you to pass this populated data, as the populated document contains data required to output the complete HTML (e.g. url). Just the ID is not enough to know how to render an image. It also cannot fetch the image document for you, as it's a synchronous function - hence it has to expect populated data.

If you want the converter to fetch the document for you, you can use the asynchronous version of the HTML converter and pass a populate function.

You must be logged in to vote
1 reply
Comment options

I'm using the async version above and it still breaks because it sees partially populated data and assumes it's fully populated.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Category
Q&A
Labels
plugin: richtext-lexical @payloadcms/richtext-lexical
Converted from issue

This discussion was converted from issue #14214 on October 22, 2025 19:25.

AltStyle によって変換されたページ (->オリジナル) /