Simplify your stack and build anything. Or everything.
Build tomorrow’s web with a modern solution you truly own.
Code-based nature means you can build on top of it to power anything.
It’s time to take back your content infrastructure.

Converting JSX

Converting Richtext to JSX

To convert richtext to JSX, import the RichText component from @payloadcms/richtext-lexical/react and pass the richtext content to it:

1
import React from 'react'
2
import { RichText } from '@payloadcms/richtext-lexical/react'
3
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
4
5
export const MyComponent = ({ data }: { data: SerializedEditorState }) => {
6
return <RichText data={data} />
7
}

The RichText component includes built-in converters for common Lexical nodes. You can add or override converters via the converters prop for custom blocks, custom nodes, or any modifications you need. See the website template for a working example.

Converting Internal Links

By default, Payload doesn't know how to convert internal links to JSX, as it doesn't know what the corresponding URL of the internal link is. You'll notice that you get a "found internal link, but internalDocToHref is not provided" error in the console when you try to render content with internal links.

To fix this, you need to pass the internalDocToHref prop to LinkJSXConverter. This prop is a function that receives the link node and returns the URL of the document.

1
import type {
2
DefaultNodeTypes,
3
SerializedLinkNode,
4
} from '@payloadcms/richtext-lexical'
5
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
6
7
import {
8
type JSXConvertersFunction,
9
LinkJSXConverter,
10
RichText,
11
} from '@payloadcms/richtext-lexical/react'
12
import React from 'react'
13
14
const internalDocToHref = ({ linkNode }: { linkNode: SerializedLinkNode }) => {
15
const { relationTo, value } = linkNode.fields.doc!
16
if (typeof value !== 'object') {
17
throw new Error('Expected value to be an object')
18
}
19
const slug = value.slug
20
21
switch (relationTo) {
22
case 'posts':
23
return `/posts/${slug}`
24
case 'categories':
25
return `/category/${slug}`
26
case 'pages':
27
return `/${slug}`
28
default:
29
return `/${relationTo}/${slug}`
30
}
31
}
32
33
const jsxConverters: JSXConvertersFunction<DefaultNodeTypes> = ({
34
defaultConverters,
35
}) => ({
36
...defaultConverters,
37
...LinkJSXConverter({ internalDocToHref }),
38
})
39
40
export const MyComponent: React.FC<{
41
lexicalData: SerializedEditorState
42
}> = ({ lexicalData }) => {
43
return <RichText converters={jsxConverters} data={lexicalData} />
44
}

Converting Lexical Blocks

If your rich text includes custom Blocks or Inline Blocks, you must supply custom converters that match each block's slug. This converter is not included by default, as Payload doesn't know how to render your custom blocks.

For example:

1
'use client'
2
import type { MyInlineBlock, MyNumberBlock, MyTextBlock } from '@/payload-types'
3
import type {
4
DefaultNodeTypes,
5
SerializedBlockNode,
6
SerializedInlineBlockNode,
7
} from '@payloadcms/richtext-lexical'
8
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
9
10
import {
11
type JSXConvertersFunction,
12
RichText,
13
} from '@payloadcms/richtext-lexical/react'
14
import React from 'react'
15
16
// Extend the default node types with your custom blocks for full type safety
17
type NodeTypes =
18
| DefaultNodeTypes
19
| SerializedBlockNode<MyNumberBlock | MyTextBlock>
20
| SerializedInlineBlockNode<MyInlineBlock>
21
22
const jsxConverters: JSXConvertersFunction<NodeTypes> = ({
23
defaultConverters,
24
}) => ({
25
...defaultConverters,
26
blocks: {
27
// Each key should match your block's slug
28
myNumberBlock: ({ node }) => <div>{node.fields.number}</div>,
29
myTextBlock: ({ node }) => (
30
<div style={{ backgroundColor: 'red' }}>{node.fields.text}</div>
31
),
32
},
33
inlineBlocks: {
34
// Each key should match your inline block's slug
35
myInlineBlock: ({ node }) => <span>{node.fields.text}</span>,
36
},
37
})
38
39
export const MyComponent: React.FC<{
40
lexicalData: SerializedEditorState
41
}> = ({ lexicalData }) => {
42
return <RichText converters={jsxConverters} data={lexicalData} />
43
}

Overriding Default JSX Converters

You can override any of the default JSX converters by passing passing your custom converter, keyed to the node type, to the converters prop / the converters function.

Example - overriding the upload node converter to use next/image:

1
'use client'
2
import type {
3
DefaultNodeTypes,
4
SerializedUploadNode,
5
} from '@payloadcms/richtext-lexical'
6
import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
7
8
import {
9
type JSXConvertersFunction,
10
RichText,
11
} from '@payloadcms/richtext-lexical/react'
12
import Image from 'next/image'
13
import React from 'react'
14
15
type NodeTypes = DefaultNodeTypes
16
17
// Custom upload converter component that uses next/image
18
const CustomUploadComponent: React.FC<{
19
node: SerializedUploadNode
20
}> = ({ node }) => {
21
if (node.relationTo === 'uploads') {
22
const uploadDoc = node.value
23
if (typeof uploadDoc !== 'object') {
24
return null
25
}
26
const { alt, height, url, width } = uploadDoc
27
return <Image alt={alt} height={height} src={url} width={width} />
28
}
29
30
return null
31
}
32
33
const jsxConverters: JSXConvertersFunction<NodeTypes> = ({
34
defaultConverters,
35
}) => ({
36
...defaultConverters,
37
// Override the default upload converter
38
upload: ({ node }) => {
39
return <CustomUploadComponent node={node} />
40
},
41
})
42
43
export const MyComponent: React.FC<{
44
lexicalData: SerializedEditorState
45
}> = ({ lexicalData }) => {
46
return <RichText converters={jsxConverters} data={lexicalData} />
47
}
Next

Converting HTML