|
2 | 2 | title: Lexical Converters
|
3 | 3 | label: Converters
|
4 | 4 | order: 20
|
5 |
| -desc: Conversion between lexical, markdown and html |
6 |
| -keywords: lexical, rich text, editor, headless cms, convert, html, mdx, markdown, md, conversion, export |
| 5 | +desc: Conversion between lexical, markdown, jsx and html |
| 6 | +keywords: lexical, rich text, editor, headless cms, convert, html, mdx, markdown, md, conversion, export, jsx |
7 | 7 | ---
|
8 | 8 |
|
9 | 9 | Lexical saves data in JSON - this is great for storage and flexibility and allows you to easily to convert it to other formats like JSX, HTML or Markdown.
|
@@ -74,20 +74,28 @@ To convert Lexical Blocks or Inline Blocks to JSX, pass the converter for your b
|
74 | 74 |
|
75 | 75 | ```tsx
|
76 | 76 | 'use client'
|
77 |
| -import type { MyInlineBlock, MyTextBlock } from '@/payload-types' |
78 |
| -import type { DefaultNodeTypes, SerializedBlockNode } from '@payloadcms/richtext-lexical' |
| 77 | +import type { MyInlineBlock, MyNumberBlock, MyTextBlock } from '@/payload-types' |
| 78 | +import type { |
| 79 | + DefaultNodeTypes, |
| 80 | + SerializedBlockNode, |
| 81 | + SerializedInlineBlockNode, |
| 82 | +} from '@payloadcms/richtext-lexical' |
79 | 83 | import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical'
|
80 | 84 |
|
81 | 85 | import { type JSXConvertersFunction, RichText } from '@payloadcms/richtext-lexical/react'
|
82 | 86 | import React from 'react'
|
83 | 87 |
|
84 | 88 | // Extend the default node types with your custom blocks for full type safety
|
85 |
| -type NodeTypes = DefaultNodeTypes | SerializedBlockNode<MyInlineBlock | MyTextBlock> |
| 89 | +type NodeTypes = |
| 90 | + | DefaultNodeTypes |
| 91 | + | SerializedBlockNode<MyNumberBlock | MyTextBlock> |
| 92 | + | SerializedInlineBlockNode<MyInlineBlock> |
86 | 93 |
|
87 | 94 | const jsxConverters: JSXConvertersFunction<NodeTypes> = ({ defaultConverters }) => ({
|
88 | 95 | ...defaultConverters,
|
89 | 96 | blocks: {
|
90 | 97 | // Each key should match your block's slug
|
| 98 | + myNumberBlock: ({ node }) => <div>{node.fields.number}</div>, |
91 | 99 | myTextBlock: ({ node }) => <div style={{ backgroundColor: 'red' }}>{node.fields.text}</div>,
|
92 | 100 | },
|
93 | 101 | inlineBlocks: {
|
@@ -155,182 +163,220 @@ export const MyComponent: React.FC<{
|
155 | 163 |
|
156 | 164 | If you don't have a React-based frontend, or if you need to send the content to a third-party service, you can convert lexical to HTML. There are two ways to do this:
|
157 | 165 |
|
158 |
| -1. **Outputting HTML from the Collection:** Create a new field in your collection to convert saved JSON content to HTML. Payload generates and outputs the HTML for use in your frontend. |
159 |
| -2. **Generating HTML on any server** Convert JSON to HTML on-demand on the server. |
160 | 166 |
|
161 |
| -In both cases, the conversion needs to happen on a server, as the HTML converter will automatically fetch data for nodes that require it (e.g. uploads and internal links). The editor comes with built-in HTML serializers, simplifying the process of converting JSON to HTML. |
| 167 | +1. **Generating HTML in your frontend** Convert JSON to HTML on-demand wherever you need it (Recommended). |
| 168 | +2. **Outputting HTML from the Collection:** Create a new field in your collection to convert saved JSON content to HTML. Payload generates and outputs the HTML for use in your frontend. This is not recommended, as this approach adds additional overhead to the Payload API and may not work with live preview. |
162 | 169 |
|
163 |
| -### Outputting HTML from the Collection |
| 170 | +### Generating HTML in your frontend |
164 | 171 |
|
165 |
| -To add HTML generation directly within the collection, follow the example below: |
| 172 | +If you wish to convert JSON to HTML ad-hoc, use the `convertLexicalToHTML` function exported from `@payloadcms/richtext-lexical/html`: |
166 | 173 |
|
167 |
| -```ts |
168 |
| -import type { CollectionConfig } from 'payload' |
| 174 | +```tsx |
| 175 | +'use client' |
169 | 176 |
|
170 |
| -import { HTMLConverterFeature, lexicalEditor, lexicalHTML } from '@payloadcms/richtext-lexical' |
| 177 | +import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical' |
| 178 | +import { convertLexicalToHTML } from '@payloadcms/richtext-lexical/html' |
171 | 179 |
|
172 |
| -const Pages: CollectionConfig = { |
173 |
| - slug: 'pages', |
174 |
| - fields: [ |
175 |
| - { |
176 |
| - name: 'nameOfYourRichTextField', |
177 |
| - type: 'richText', |
178 |
| - editor: lexicalEditor({ |
179 |
| - features: ({ defaultFeatures }) => [ |
180 |
| - ...defaultFeatures, |
181 |
| - // The HTMLConverter Feature is the feature which manages the HTML serializers. |
182 |
| - // If you do not pass any arguments to it, it will use the default serializers. |
183 |
| - HTMLConverterFeature({}), |
184 |
| - ], |
185 |
| - }), |
186 |
| - }, |
187 |
| - lexicalHTML('nameOfYourRichTextField', { name: 'nameOfYourRichTextField_html' }), |
188 |
| - ], |
| 180 | +import React from 'react' |
| 181 | + |
| 182 | +export const MyComponent = ({ data }: { data: SerializedEditorState }) => { |
| 183 | + const html = convertLexicalToHTML({ data }) |
| 184 | + |
| 185 | + return <div dangerouslySetInnerHTML={{ __html: html }} /> |
189 | 186 | }
|
190 | 187 | ```
|
191 | 188 |
|
192 |
| -The `lexicalHTML()` function creates a new field that automatically converts the referenced lexical richText field into HTML through an afterRead hook. |
| 189 | +### Generating HTML in your frontend with dynamic population |
193 | 190 |
|
194 |
| -### Generating HTML anywhere on the server |
| 191 | +The default `convertLexicalToHTML` function does not populate data for nodes like uploads or links - it expects you to pass in the fully populated data. If you want the converter to dynamically populate those nodes as they are encountered, you have to use the async version of the converter, imported from `@payloadcms/richtext-lexical/html-async`, and pass in the `populate` function: |
195 | 192 |
|
196 |
| -If you wish to convert JSON to HTML ad-hoc, use the `convertLexicalToHTML` function: |
| 193 | +```tsx |
| 194 | +'use client' |
197 | 195 |
|
198 |
| -```ts |
199 |
| -import { consolidateHTMLConverters, convertLexicalToHTML } from '@payloadcms/richtext-lexical' |
| 196 | +import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical' |
200 | 197 |
|
| 198 | +import { getRestPopulateFn } from '@payloadcms/richtext-lexical/client' |
| 199 | +import { convertLexicalToHTMLAsync } from '@payloadcms/richtext-lexical/html-async' |
| 200 | +import React, { useEffect, useState } from 'react' |
201 | 201 |
|
202 |
| -await convertLexicalToHTML({ |
203 |
| - converters: consolidateHTMLConverters({ editorConfig }), |
204 |
| - data: editorData, |
205 |
| - payload, // if you have Payload but no req available, pass it in here to enable server-only functionality (e.g. proper conversion of upload nodes) |
206 |
| - req, // if you have req available, pass it in here to enable server-only functionality (e.g. proper conversion of upload nodes). No need to pass in Payload if req is passed in. |
207 |
| -}) |
208 |
| -``` |
209 |
| -This method employs `convertLexicalToHTML` from `@payloadcms/richtext-lexical`, which converts the serialized editor state into HTML. |
| 202 | +export const MyComponent = ({ data }: { data: SerializedEditorState }) => { |
| 203 | + const [html, setHTML] = useState<null | string>(null) |
| 204 | + useEffect(() => { |
| 205 | + async function convert() { |
| 206 | + const html = await convertLexicalToHTMLAsync({ |
| 207 | + data, |
| 208 | + populate: getRestPopulateFn({ |
| 209 | + apiURL: `http://localhost:3000/api`, |
| 210 | + }), |
| 211 | + }) |
| 212 | + setHTML(html) |
| 213 | + } |
210 | 214 |
|
211 |
| -Because every `Feature` is able to provide html converters, and because the `htmlFeature` can modify those or provide their own, we need to consolidate them with the default html Converters using the `consolidateHTMLConverters` function. |
| 215 | + void convert() |
| 216 | + }, [data]) |
212 | 217 |
|
213 |
| -#### Example: Generating HTML within an afterRead hook |
| 218 | + return html && <div dangerouslySetInnerHTML={{ __html: html }} /> |
| 219 | +} |
| 220 | +``` |
214 | 221 |
|
215 |
| -```ts |
216 |
| -import type { FieldHook } from 'payload' |
| 222 | +Do note that using the REST populate function will result in each node sending a separate request to the REST API, which may be slow for a large amount of nodes. On the server, you can use the payload populate function, which will be more efficient: |
217 | 223 |
|
218 |
| -import { |
219 |
| - HTMLConverterFeature, |
220 |
| - consolidateHTMLConverters, |
221 |
| - convertLexicalToHTML, |
222 |
| - defaultEditorConfig, |
223 |
| - defaultEditorFeatures, |
224 |
| - sanitizeServerEditorConfig, |
225 |
| -} from '@payloadcms/richtext-lexical' |
| 224 | +```tsx |
| 225 | +import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical' |
226 | 226 |
|
227 |
| -const hook: FieldHook = async ({ req, siblingData }) => { |
228 |
| - const editorConfig = defaultEditorConfig |
| 227 | +import { getPayloadPopulateFn } from '@payloadcms/richtext-lexical' |
| 228 | +import { convertLexicalToHTMLAsync } from '@payloadcms/richtext-lexical/html-async' |
| 229 | +import { getPayload } from 'payload' |
| 230 | +import React from 'react' |
229 | 231 |
|
230 |
| - editorConfig.features = [...defaultEditorFeatures, HTMLConverterFeature({})] |
| 232 | +import config from '../../config.js' |
231 | 233 |
|
232 |
| - const sanitizedEditorConfig = await sanitizeServerEditorConfig(editorConfig, req.payload.config) |
| 234 | +export const MyRSCComponent = async ({ data }: { data: SerializedEditorState }) => { |
| 235 | + const payload = await getPayload({ |
| 236 | + config, |
| 237 | + }) |
233 | 238 |
|
234 |
| - const html = await convertLexicalToHTML({ |
235 |
| - converters: consolidateHTMLConverters({ editorConfig: sanitizedEditorConfig }), |
236 |
| - data: siblingData.lexicalSimple, |
237 |
| - req, |
| 239 | + const html = await convertLexicalToHTMLAsync({ |
| 240 | + data, |
| 241 | + populate: await getPayloadPopulateFn({ |
| 242 | + currentDepth: 0, |
| 243 | + depth: 1, |
| 244 | + payload, |
| 245 | + }), |
238 | 246 | })
|
239 |
| - return html |
| 247 | + |
| 248 | + return html && <div dangerouslySetInnerHTML={{ __html: html }} /> |
240 | 249 | }
|
241 | 250 | ```
|
242 | 251 |
|
243 |
| -### CSS |
| 252 | +### Converting Lexical Blocks |
244 | 253 |
|
245 |
| -Payload's lexical HTML converter does not generate CSS for you, but it does add classes to the generated HTML. You can use these classes to style the HTML in your frontend. |
| 254 | +```tsx |
| 255 | +'use client' |
246 | 256 |
|
247 |
| -Here is some "base" CSS you can use to ensure that nested lists render correctly: |
| 257 | +import type { MyInlineBlock, MyTextBlock } from '@/payload-types' |
| 258 | +import type { |
| 259 | + DefaultNodeTypes, |
| 260 | + SerializedBlockNode, |
| 261 | + SerializedInlineBlockNode, |
| 262 | +} from '@payloadcms/richtext-lexical' |
| 263 | +import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical' |
248 | 264 |
|
249 |
| -```css |
250 |
| -/* Base CSS for Lexical HTML */ |
251 |
| -.nestedListItem, .list-check { |
252 |
| - list-style-type: none; |
| 265 | +import { |
| 266 | + convertLexicalToHTML, |
| 267 | + type HTMLConvertersFunction, |
| 268 | +} from '@payloadcms/richtext-lexical/html' |
| 269 | +import React from 'react' |
| 270 | + |
| 271 | +type NodeTypes = |
| 272 | + | DefaultNodeTypes |
| 273 | + | SerializedBlockNode<MyTextBlock> |
| 274 | + | SerializedInlineBlockNode<MyInlineBlock> |
| 275 | + |
| 276 | +const htmlConverters: HTMLConvertersFunction<NodeTypes> = ({ defaultConverters }) => ({ |
| 277 | + ...defaultConverters, |
| 278 | + blocks: { |
| 279 | + // Each key should match your block's slug |
| 280 | + myTextBlock: ({ node, providedCSSString }) => |
| 281 | + `<div style="background-color: red;${providedCSSString}">${node.fields.text}</div>`, |
| 282 | + }, |
| 283 | + inlineBlocks: { |
| 284 | + // Each key should match your inline block's slug |
| 285 | + myInlineBlock: ({ node, providedStyleTag }) => |
| 286 | + `<span${providedStyleTag}>${node.fields.text}</span$>`, |
| 287 | + }, |
| 288 | +}) |
| 289 | + |
| 290 | +export const MyComponent = ({ data }: { data: SerializedEditorState }) => { |
| 291 | + const html = convertLexicalToHTML({ |
| 292 | + converters: htmlConverters, |
| 293 | + data, |
| 294 | + }) |
| 295 | + |
| 296 | + return <div dangerouslySetInnerHTML={{ __html: html }} /> |
253 | 297 | }
|
254 | 298 | ```
|
255 | 299 |
|
256 |
| -### Creating your own HTML Converter |
| 300 | +### Outputting HTML from the Collection |
257 | 301 |
|
258 |
| -HTML Converters are typed as `HTMLConverter`, which contains the node type it should handle, and a function that accepts the serialized node from the lexical editor, and outputs the HTML string. Here's the HTML Converter of the Upload node as an example: |
| 302 | +To add HTML generation directly within the collection, follow the example below: |
259 | 303 |
|
260 | 304 | ```ts
|
261 |
| -import type { HTMLConverter } from '@payloadcms/richtext-lexical' |
262 |
| - |
263 |
| -const UploadHTMLConverter: HTMLConverter<SerializedUploadNode> = { |
264 |
| - converter: async ({ node, req }) => { |
265 |
| - const uploadDocument: { |
266 |
| - value?: any |
267 |
| - } = {} |
268 |
| - if(req) { |
269 |
| - await populate({ |
270 |
| - id, |
271 |
| - collectionSlug: node.relationTo, |
272 |
| - currentDepth: 0, |
273 |
| - data: uploadDocument, |
274 |
| - depth: 1, |
275 |
| - draft: false, |
276 |
| - key: 'value', |
277 |
| - overrideAccess: false, |
278 |
| - req, |
279 |
| - showHiddenFields: false, |
280 |
| - }) |
281 |
| - } |
282 |
| - |
283 |
| - const url = (req?.payload?.config?.serverURL || '') + uploadDocument?.value?.url |
| 305 | +import type { HTMLConvertersFunction } from '@payloadcms/richtext-lexical/html' |
| 306 | +import type { MyTextBlock } from '@/payload-types.js' |
| 307 | +import type { CollectionConfig } from 'payload' |
284 | 308 |
|
285 |
| - if (!(uploadDocument?.value?.mimeType as string)?.startsWith('image')) { |
286 |
| - // Only images can be serialized as HTML |
287 |
| - return `` |
288 |
| - } |
| 309 | +import { |
| 310 | + BlocksFeature, |
| 311 | + type DefaultNodeTypes, |
| 312 | + lexicalEditor, |
| 313 | + lexicalHTMLField, |
| 314 | + type SerializedBlockNode, |
| 315 | +} from '@payloadcms/richtext-lexical' |
289 | 316 |
|
290 |
| - return `<img src="${url}" alt="${uploadDocument?.value?.filename}" width="${uploadDocument?.value?.width}" height="${uploadDocument?.value?.height}"/>` |
291 |
| - }, |
292 |
| - nodeTypes: [UploadNode.getType()], // This is the type of the lexical node that this converter can handle. Instead of hardcoding 'upload' we can get the node type directly from the UploadNode, since it's static. |
| 317 | +const Pages: CollectionConfig = { |
| 318 | + slug: 'pages', |
| 319 | + fields: [ |
| 320 | + { |
| 321 | + name: 'nameOfYourRichTextField', |
| 322 | + type: 'richText', |
| 323 | + editor: lexicalEditor(), |
| 324 | + }, |
| 325 | + lexicalHTMLField({ |
| 326 | + htmlFieldName: 'nameOfYourRichTextField_html', |
| 327 | + lexicalFieldName: 'nameOfYourRichTextField', |
| 328 | + }), |
| 329 | + { |
| 330 | + name: 'customRichText', |
| 331 | + type: 'richText', |
| 332 | + editor: lexicalEditor({ |
| 333 | + features: ({ defaultFeatures }) => [ |
| 334 | + ...defaultFeatures, |
| 335 | + BlocksFeature({ |
| 336 | + blocks: [ |
| 337 | + { |
| 338 | + interfaceName: 'MyTextBlock', |
| 339 | + slug: 'myTextBlock', |
| 340 | + fields: [ |
| 341 | + { |
| 342 | + name: 'text', |
| 343 | + type: 'text', |
| 344 | + }, |
| 345 | + ], |
| 346 | + }, |
| 347 | + ], |
| 348 | + }), |
| 349 | + ], |
| 350 | + }), |
| 351 | + }, |
| 352 | + lexicalHTMLField({ |
| 353 | + htmlFieldName: 'customRichText_html', |
| 354 | + lexicalFieldName: 'customRichText', |
| 355 | + // can pass in additional converters or override default ones |
| 356 | + converters: (({ defaultConverters }) => ({ |
| 357 | + ...defaultConverters, |
| 358 | + blocks: { |
| 359 | + myTextBlock: ({ node, providedCSSString }) => |
| 360 | + `<div style="background-color: red;${providedCSSString}">${node.fields.text}</div>`, |
| 361 | + }, |
| 362 | + })) as HTMLConvertersFunction<DefaultNodeTypes | SerializedBlockNode<MyTextBlock>>, |
| 363 | + }), |
| 364 | + ], |
293 | 365 | }
|
294 | 366 | ```
|
295 | 367 |
|
296 |
| -As you can see, we have access to all the information saved in the node (for the Upload node, this is `value`and `relationTo`) and we can use that to generate the HTML. |
| 368 | +The `lexicalHTML()` function creates a new field that automatically converts the referenced lexical richText field into HTML through an afterRead hook. |
297 | 369 |
|
298 |
| -The `convertLexicalToHTML` is part of `@payloadcms/richtext-lexical` automatically handles traversing the editor state and calling the correct converter for each node. |
| 370 | +### CSS |
299 | 371 |
|
300 |
| -### Embedding the HTML Converter in your Feature |
| 372 | +Payload's lexical HTML converter does not generate CSS for you, but it does add classes to the generated HTML. You can use these classes to style the HTML in your frontend. |
301 | 373 |
|
302 |
| -You can embed your HTML Converter directly within your custom `ServerFeature`, allowing it to be handled automatically by the `consolidateHTMLConverters` function. Here is an example: |
| 374 | +Here is some "base" CSS you can use to ensure that nested lists render correctly: |
303 | 375 |
|
304 |
| -```ts |
305 |
| -import { createNode } from '@payloadcms/richtext-lexical' |
306 |
| -import type { FeatureProviderProviderServer } from '@payloadcms/richtext-lexical' |
307 |
| - |
308 |
| -export const UploadFeature: FeatureProviderProviderServer< |
309 |
| - UploadFeatureProps, |
310 |
| - UploadFeaturePropsClient |
311 |
| -> = (props) => { |
312 |
| - /*...*/ |
313 |
| - return { |
314 |
| - feature: () => { |
315 |
| - return { |
316 |
| - nodes: [ |
317 |
| - createNode({ |
318 |
| - converters: { |
319 |
| - html: yourHTMLConverter, // <= This is where you define your HTML Converter |
320 |
| - }, |
321 |
| - node: UploadNode, |
322 |
| - //... |
323 |
| - }), |
324 |
| - ], |
325 |
| - ClientComponent: UploadFeatureClientComponent, |
326 |
| - clientFeatureProps: clientProps, |
327 |
| - serverFeatureProps: props, |
328 |
| - /*...*/ |
329 |
| - } |
330 |
| - }, |
331 |
| - key: 'upload', |
332 |
| - serverFeatureProps: props, |
333 |
| - } |
| 376 | +```css |
| 377 | +/* Base CSS for Lexical HTML */ |
| 378 | +.nestedListItem, .list-check { |
| 379 | + list-style-type: none; |
334 | 380 | }
|
335 | 381 | ```
|
336 | 382 |
|
|
0 commit comments