Back to Payload

Converting Markdown

docs/rich-text/converting-markdown.mdx

3.84.18.4 KB
Original Source

Richtext to Markdown

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:

ts
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
  }),
})

Example - outputting Markdown from the Collection

ts
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
          },
        ],
      },
    },
  ],
}

Markdown to Richtext

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:

ts
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**.',
})

Converting Uploads

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:

ts
![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).

Migrating existing Markdown content

If you're migrating content that contains standard ![alt] (url) image references, the recommended approach is:

  1. Upload your images to the Media collection first to get their document IDs
  2. Pre-process your Markdown to replace ![alt] (url) with ![media:<id>]()
  3. Then run convertMarkdownToLexical

Converting MDX

Payload 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.

Defining a Custom Block

Here is an example of a Banner block.

This block:

  • Renders in the admin UI as a normal Lexical block with specific fields (e.g. type, content).
  • Converts to an MDX Banner component.
  • Can parse that MDX 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" />

ts
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.

Export

The export function takes the block field data and the lexicalToMarkdown function as arguments. It returns the following object:

PropertyTypeDescription
childrenstringThis will be in between the opening and closing tags of the block.
propsobjectThis will be in the opening tag of the block.

Import

The import function provides data extracted from the MDX. It takes the following arguments:

ArgumentTypeDescription
childrenstringThis will be the text between the opening and closing tags of the block.
propsobjectThese 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.