docs/rich-text/converting-markdown.mdx
If you have access to the Payload Config and the lexical editor config, you can convert the lexical editor state to Markdown with the following:
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
import {
convertLexicalToMarkdown,
editorConfigFactory,
} from '@payloadcms/richtext-lexical'
// Your richtext data here
const data: SerializedEditorState = {}
const markdown = convertLexicalToMarkdown({
data,
editorConfig: await editorConfigFactory.default({
config, // <= make sure you have access to your Payload Config
}),
})
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
import type { CollectionConfig, RichTextField } from 'payload'
import {
convertLexicalToMarkdown,
editorConfigFactory,
lexicalEditor,
} from '@payloadcms/richtext-lexical'
const Pages: CollectionConfig = {
slug: 'pages',
fields: [
{
name: 'nameOfYourRichTextField',
type: 'richText',
editor: lexicalEditor(),
},
{
name: 'markdown',
type: 'textarea',
admin: {
hidden: true,
},
hooks: {
afterRead: [
({ siblingData, siblingFields }) => {
const data: SerializedEditorState =
siblingData['nameOfYourRichTextField']
if (!data) {
return ''
}
const markdown = convertLexicalToMarkdown({
data,
editorConfig: editorConfigFactory.fromField({
field: siblingFields.find(
(field) =>
'name' in field && field.name === 'nameOfYourRichTextField',
) as RichTextField,
}),
})
return markdown
},
],
beforeChange: [
({ siblingData }) => {
// Ensure that the markdown field is not saved in the database
delete siblingData['markdown']
return null
},
],
},
},
],
}
If you have access to the Payload Config and the lexical editor config, you can convert Markdown to the lexical editor state with the following:
import {
convertMarkdownToLexical,
editorConfigFactory,
} from '@payloadcms/richtext-lexical'
const lexicalJSON = convertMarkdownToLexical({
editorConfig: await editorConfigFactory.default({
config, // <= make sure you have access to your Payload Config
}),
markdown: '# Hello world\n\nThis is a **test**.',
})
When converting Markdown to Lexical, standard image syntax ![alt] (url) is not automatically converted to Upload nodes, because Payload has no way to look up a Media document from a URL alone.
The UploadFeature includes a built-in transformer that recognizes a special placeholder format:
![media:6507f7b9a4d3c2e1f0ab1234]()
Where media is your upload collection slug and the second part is the document ID. When convertMarkdownToLexical processes this, it creates a proper Upload node with the correct relationTo and value.
When converting from Lexical to Markdown, Upload nodes are serialized back into this same placeholder format (unless the document is populated, in which case the URL and alt text are used directly as a standard markdown image).
If you're migrating content that contains standard ![alt] (url) image references, the recommended approach is:
![alt] (url) with ![media:<id>]()convertMarkdownToLexicalPayload supports serializing and deserializing MDX content. While Markdown converters are stored on the features, MDX converters are stored on the blocks that you pass to the BlocksFeature.
Here is an example of a Banner block.
This block:
Banner component.Banner back into a Lexical state.<LightDarkImage srcLight="https://payloadcms.com/images/docs/mdx-example-light.png" srcDark="https://payloadcms.com/images/docs/mdx-example-dark.png" alt="Shows the Banner field in a lexical editor and the MDX output" caption="Banner field in a lexical editor and the MDX output" />
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
import type { Block, CollectionConfig, RichTextField } from 'payload'
import {
BlocksFeature,
convertLexicalToMarkdown,
editorConfigFactory,
lexicalEditor,
} from '@payloadcms/richtext-lexical'
const BannerBlock: Block = {
slug: 'Banner',
fields: [
{
name: 'type',
type: 'select',
defaultValue: 'info',
options: [
{ label: 'Info', value: 'info' },
{ label: 'Warning', value: 'warning' },
{ label: 'Error', value: 'error' },
],
},
{
name: 'content',
type: 'richText',
editor: lexicalEditor(),
},
],
jsx: {
/**
* Convert from Lexical -> MDX:
* <Banner type="..." >child content</Banner>
*/
export: ({ fields, lexicalToMarkdown }) => {
const props: any = {}
if (fields.type) {
props.type = fields.type
}
return {
children: lexicalToMarkdown({ editorState: fields.content }),
props,
}
},
/**
* Convert from MDX -> Lexical:
*/
import: ({ children, markdownToLexical, props }) => {
return {
type: props?.type,
content: markdownToLexical({ markdown: children }),
}
},
},
}
const Pages: CollectionConfig = {
slug: 'pages',
fields: [
{
name: 'nameOfYourRichTextField',
type: 'richText',
editor: lexicalEditor({
features: ({ defaultFeatures }) => [
...defaultFeatures,
BlocksFeature({
blocks: [BannerBlock],
}),
],
}),
},
{
name: 'markdown',
type: 'textarea',
hooks: {
afterRead: [
({ siblingData, siblingFields }) => {
const data: SerializedEditorState =
siblingData['nameOfYourRichTextField']
if (!data) {
return ''
}
const markdown = convertLexicalToMarkdown({
data,
editorConfig: editorConfigFactory.fromField({
field: siblingFields.find(
(field) =>
'name' in field && field.name === 'nameOfYourRichTextField',
) as RichTextField,
}),
})
return markdown
},
],
beforeChange: [
({ siblingData }) => {
// Ensure that the markdown field is not saved in the database
delete siblingData['markdown']
return null
},
],
},
},
],
}
The conversion is done using the jsx property of the block. The export function is called when converting from lexical to MDX, and the import function is called when converting from MDX to lexical.
The export function takes the block field data and the lexicalToMarkdown function as arguments. It returns the following object:
| Property | Type | Description |
|---|---|---|
children | string | This will be in between the opening and closing tags of the block. |
props | object | This will be in the opening tag of the block. |
The import function provides data extracted from the MDX. It takes the following arguments:
| Argument | Type | Description |
|---|---|---|
children | string | This will be the text between the opening and closing tags of the block. |
props | object | These are the props passed to the block, parsed from the opening tag into an object. |
The returning object is equal to the block field data.