> ## Documentation Index
> Fetch the complete documentation index at: https://docs.frogbot.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Converting HTML

> Convert Lexical rich text to and from HTML.

## Rich Text to HTML

There are two main approaches:

1. Generate HTML on demand where it is consumed. This avoids duplicate storage and is recommended.
2. Add an HTML field whose read hook converts a sibling Rich Text field. This adds API work and may duplicate content.

Use the synchronous converter when records and files are already populated:

```tsx theme={"theme":"tokyo-night"}
import type { SerializedEditorState } from '@frogbotai/richtext-lexical/lexical'
import { convertLexicalToHTML } from '@frogbotai/richtext-lexical/html'

export function ArticleBody({ data }: { data: SerializedEditorState }) {
  const html = convertLexicalToHTML({ data })

  return <div dangerouslySetInnerHTML={{ __html: html }} />
}
```

Only render trusted content with `dangerouslySetInnerHTML`. Sanitize untrusted HTML according to your application's security requirements.

| Option | Type | Default | Description |
| - | - | - | - |
| `data` | `SerializedEditorState` | Required | The rich text value to convert. |
| `converters` | `HTMLConverters \| ({ defaultConverters }) => HTMLConverters` | Built-in converters | Converters keyed by node type, with custom blocks under `blocks` and `inlineBlocks` keyed by slug, and an optional `unknown` fallback. An object replaces the built-in converters entirely; a function receives them as `defaultConverters` so you can extend them. See [Blocks to HTML](#blocks-to-html). |
| `className` | `string` | Built-in class name | Class name of the wrapping `div`. |
| `disableContainer` | `boolean` | `false` | Returns the HTML without the wrapping `div`. |
| `disableIndent` | `boolean \| string[]` | `false` | Omits indentation styles. `true` disables them for every node; an array disables them for the listed node types. |
| `disableTextAlign` | `boolean \| string[]` | `false` | Omits text-alignment styles. `true` disables them for every node; an array disables them for the listed node types. |

Empty editor state, including a lone empty paragraph, converts to an empty wrapping `div`, or an empty string with `disableContainer`.

### Dynamic population

The async converter can populate relationships and files while converting. In a server component, create a FrogBot instance and pass the asynchronous population function:

```tsx theme={"theme":"tokyo-night"}
import config from '@frogbot-config'
import type { SerializedEditorState } from '@frogbotai/richtext-lexical/lexical'
import { convertLexicalToHTMLAsync } from '@frogbotai/richtext-lexical/html-async'
import { getFrogBotPopulateFn } from '@frogbotai/richtext-lexical'
import { getFrogBot } from 'frogbot'

export async function ArticleBody({ data }: { data: SerializedEditorState }) {
  const frogbot = await getFrogBot({ config })
  const populate = await getFrogBotPopulateFn({
    currentDepth: 0,
    depth: 1,
    frogbot,
  })
  const html = await convertLexicalToHTMLAsync({ data, populate })

  return <div dangerouslySetInnerHTML={{ __html: html }} />
}
```

Pass an existing FrogBot request instead of `frogbot` when conversion must preserve its user, locale, and transaction context. Population skips access control unless you set `overrideAccess: false`.

```ts theme={"theme":"tokyo-night"}
const populate = await getFrogBotPopulateFn({
  currentDepth: 0,
  depth: 2,
  req,
})
```

`convertLexicalToHTMLAsync` accepts every [`convertLexicalToHTML` option](#rich-text-to-html), with async converters, plus `populate`:

| Option | Type | Default | Description |
| - | - | - | - |
| `populate` | `HTMLPopulateFn` | None | Loads a record by collection slug and ID. Every async converter receives it as `populate`. The built-in file converter uses it when a file node holds an ID instead of a populated record, and renders an empty string without it. |

`getFrogBotPopulateFn` options:

| Option | Type | Default | Description |
| - | - | - | - |
| `frogbot` | `FrogBot` | Required unless `req` is set | FrogBot instance used to create a request. Pass exactly one of `frogbot` and `req`; passing both or neither throws. |
| `req` | `FrogBotRequest` | Required unless `frogbot` is set | Existing request whose user, locale, and transaction are reused. |
| `currentDepth` | `number` | Required | Depth already reached by the caller. Use `0` at the top level. |
| `depth` | `number` | Required | Maximum population depth. |
| `locale` | `string \| null` | Default locale | Locale of the request created from `frogbot`. Ignored when `req` is set; the request's locale applies. |
| `draft` | `boolean` | `false` | Loads draft versions of populated records. |
| `overrideAccess` | `boolean` | `true` | Skips access control while populating. Set it to `false` to apply the request user's permissions. |
| `showHiddenFields` | `boolean` | `false` | Includes hidden fields in populated records. |

Client code can use `getRestPopulateFn` from `@frogbotai/richtext-lexical/client`. It makes a request for each node that needs population, so server-side population is preferable for larger documents.

```tsx theme={"theme":"tokyo-night"}
'use client'

import type { SerializedEditorState } from '@frogbotai/richtext-lexical/lexical'
import { getRestPopulateFn } from '@frogbotai/richtext-lexical/client'
import { convertLexicalToHTMLAsync } from '@frogbotai/richtext-lexical/html-async'
import { useEffect, useState } from 'react'

export function ArticleBody({ data }: { data: SerializedEditorState }) {
  const [html, setHTML] = useState<string | null>(null)

  useEffect(() => {
    void convertLexicalToHTMLAsync({
      data,
      populate: getRestPopulateFn({ apiURL: '/api' }),
    }).then(setHTML)
  }, [data])

  return html ? <div dangerouslySetInnerHTML={{ __html: html }} /> : null
}
```

`getRestPopulateFn` sends a `GET` request to `<apiURL>/<collection>/<id>` with the browser's cookies.

| Option | Type | Default | Description |
| - | - | - | - |
| `apiURL` | `string` | Required | Base URL of the REST API, such as `/api` or `https://example.com/api`. |
| `depth` | `number` | `0` | `depth` query parameter for each request. |
| `draft` | `boolean` | `false` | Requests draft versions. |
| `locale` | `string` | Default locale | `locale` query parameter for each request. |

## HTML field

`lexicalHTMLField` creates a code field whose `afterRead` hook converts a sibling Rich Text field to HTML.

```ts theme={"theme":"tokyo-night"}
import type { CollectionConfig } from 'frogbot'
import {
  lexicalEditor,
  lexicalHTMLField,
} from '@frogbotai/richtext-lexical'

export const Pages: CollectionConfig = {
  slug: 'pages',
  fields: [
    {
      name: 'content',
      type: 'richText',
      editor: lexicalEditor({}),
    },
    lexicalHTMLField({
      htmlFieldName: 'contentHTML',
      lexicalFieldName: 'content',
    }),
  ],
}
```

The helper generates HTML when the record is read. By default, it removes the generated field before database writes, avoiding duplicate stored content. Set `storeInDB: true` to retain a caller-supplied value, but conversion still occurs on read rather than automatically during writes.

| Option | Type | Default | Description |
| - | - | - | - |
| `htmlFieldName` | `string` | Required | Name of the generated code field that holds the HTML. |
| `lexicalFieldName` | `string` | Required | Name of the sibling Rich Text field to convert. An empty value produces an empty string. |
| `converters` | `HTMLConvertersAsync \| ({ defaultConverters }) => HTMLConvertersAsync` | Built-in converters | Async converters, as for [`convertLexicalToHTMLAsync`](#dynamic-population). |
| `hidden` | `boolean` | `true` | Hides the generated field in the Admin Panel. |
| `storeInDB` | `boolean` | `false` | Keeps a value written to the field instead of removing it before saving. |

The read hook populates file nodes using the read operation's request, depth, draft, access, and hidden-field settings, and wraps the output in a `div` with the class `frogbot-richtext`.

### Custom HTML field converters

```ts theme={"theme":"tokyo-night"}
import type { CollectionConfig } from 'frogbot'
import {
  BlocksFeature,
  type DefaultNodeTypes,
  lexicalEditor,
  lexicalHTMLField,
  type SerializedBlockNode,
} from '@frogbotai/richtext-lexical'
import type { HTMLConvertersFunctionAsync } from '@frogbotai/richtext-lexical/html-async'

type ArticleCallout = {
  blockName?: string
  blockType: 'callout'
  text: string
}

const converters: HTMLConvertersFunctionAsync<
  DefaultNodeTypes | SerializedBlockNode<ArticleCallout>
> = ({ defaultConverters }) => ({
  ...defaultConverters,
  blocks: {
    callout: async ({ node, providedCSSString }) =>
      `<aside style="${providedCSSString}">${node.fields.text}</aside>`,
  },
})

export const Articles: CollectionConfig = {
  slug: 'articles',
  fields: [
    {
      name: 'content',
      type: 'richText',
      editor: lexicalEditor({
        features: ({ defaultFeatures }) => [
          ...defaultFeatures,
          BlocksFeature({
            blocks: [
              {
                slug: 'callout',
                fields: [{ name: 'text', type: 'text', required: true }],
              },
            ],
          }),
        ],
      }),
    },
    lexicalHTMLField({
      converters,
      htmlFieldName: 'contentHTML',
      lexicalFieldName: 'content',
    }),
  ],
}
```

## Blocks to HTML

Supply block converters by slug when converting on demand:

```ts theme={"theme":"tokyo-night"}
import type {
  DefaultNodeTypes,
  SerializedBlockNode,
  SerializedInlineBlockNode,
} from '@frogbotai/richtext-lexical'
import type { HTMLConvertersFunction } from '@frogbotai/richtext-lexical/html'

type BadgeBlock = {
  blockName?: string
  blockType: 'badge'
  label: string
}

type CalloutBlock = {
  blockName?: string
  blockType: 'callout'
  text: string
}

type NodeTypes =
  | DefaultNodeTypes
  | SerializedBlockNode<CalloutBlock>
  | SerializedInlineBlockNode<BadgeBlock>

const converters: HTMLConvertersFunction<NodeTypes> = ({ defaultConverters }) => ({
  ...defaultConverters,
  blocks: {
    callout: ({ node, providedCSSString }) => {
      const fields = node.fields

      return `<aside style="${providedCSSString}">${'text' in fields ? fields.text : ''}</aside>`
    },
  },
  inlineBlocks: {
    badge: ({ node, providedStyleTag }) => {
      const fields = node.fields

      return `<span${providedStyleTag}>${'label' in fields ? fields.label : ''}</span>`
    },
  },
})

export { converters }
```

Pass this converter function as `converters` when using an HTML conversion API whose node-type parameter matches `NodeTypes`.

## HTML to Rich Text

`convertHTMLToLexical` needs a DOM implementation and the editor config that defines allowed nodes:

```ts theme={"theme":"tokyo-night"}
import config from '@frogbot-config'
import {
  convertHTMLToLexical,
  editorConfigFactory,
} from '@frogbotai/richtext-lexical'
import { JSDOM } from 'jsdom'

const data = convertHTMLToLexical({
  editorConfig: await editorConfigFactory.default({ config }),
  html: '<h2>Introduction</h2><p>Article text.</p>',
  JSDOM,
})
```

Install `jsdom` and its types in projects that run this conversion on the server.

| Option | Type | Default | Description |
| - | - | - | - |
| `html` | `string` | Required | HTML to convert. |
| `editorConfig` | `SanitizedServerEditorConfig` | Required | Editor config whose features define the nodes the HTML can become. See [Retrieving the editor config](https://docs.frogbot.ai/rich-text/converters.md#retrieving-the-editor-config). |
| `JSDOM` | `new (html: string) => { window: { document: Document } }` | Required | DOM implementation used to parse the HTML, such as the `JSDOM` class from `jsdom`. |

Image tags are not uploaded automatically. FrogBot cannot choose an upload
collection or satisfy that collection's required fields without your input.
Images without the required Lexical data attributes are omitted.

## Converting HTML with images

Image tags are not uploaded automatically. Upload each file first, then add its record ID and collection slug to the source HTML:

```ts theme={"theme":"tokyo-night"}
import config from '@frogbot-config'
import {
  convertHTMLToLexical,
  editorConfigFactory,
} from '@frogbotai/richtext-lexical'
import { getFrogBot } from 'frogbot'
import { JSDOM } from 'jsdom'

const frogbot = await getFrogBot({ config })
const uploadedFile = await frogbot.create({
  collection: 'files',
  overrideAccess: true,
  data: { alt: 'Mountain at sunrise' },
  filePath: '/imports/mountain.jpg',
})

const html = `
  <p>Travel notes</p>
  <img
    src="${uploadedFile.url}"
    data-lexical-upload-id="${uploadedFile.id}"
    data-lexical-upload-relation-to="files"
    alt="Mountain at sunrise"
  />
`

const data = convertHTMLToLexical({
  editorConfig: await editorConfigFactory.default({ config }),
  html,
  JSDOM,
})
```

The required attributes are `data-lexical-upload-id` and `data-lexical-upload-relation-to`. For external images, download and upload each image, update its DOM element with these attributes, then convert the updated HTML.

For bulk migration, parse the source with `JSDOM`, iterate over its `img` elements, upload each image with `frogbot.create`, and set both attributes before converting `document.body.innerHTML`. Handle download or upload failures explicitly so a partial migration is visible rather than silently losing images.

If you already have file IDs, `buildEditorState` can construct a state directly:

```ts theme={"theme":"tokyo-night"}
import { buildEditorState } from '@frogbotai/richtext-lexical'

const data = buildEditorState({
  text: 'Travel notes',
  nodes: [
    {
      type: 'upload',
      fields: {},
      format: '',
      id: crypto.randomUUID(),
      relationTo: 'files',
      value: 'file-record-id',
      version: 3,
    },
  ],
})
```

Verify that each referenced file ID exists and that `relationTo` matches the upload collection slug. Server-side conversion can only reference saved files, not uploads that are still pending in the admin panel.

### buildEditorState options

`buildEditorState` is exported from both the package root and `@frogbotai/richtext-lexical/client`. Pass the allowed node types as its type argument, such as `buildEditorState<DefaultNodeTypes>(...)`, to type the result.

| Option | Type | Default | Description |
| - | - | - | - |
| `text` | `string` | None | Creates a first paragraph containing this text. |
| `nodes` | `SerializedLexicalNode[]` | None | Top-level nodes added after the paragraph created from `text`, or as the only content when `text` is not set. |

With neither option, the result is an editor state with no content.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.