The @platejs/markdown package provides robust, two-way conversion between Markdown and Plate's content structure.
Features
- Markdown to Plate JSON: Convert Markdown strings to Plate's editable format (
deserialize). - Plate JSON to Markdown: Convert Plate content back to Markdown strings (
serialize). - Safe by Default: Handles Markdown conversion without
dangerouslySetInnerHTML. - Customizable Rules: Define how specific Markdown syntax or custom Plate elements are converted using
rules. Supports MDX. - Extensible: Utilize remark plugins via the
remarkPluginsoption. - Compliant: Supports CommonMark, with GFM (GitHub Flavored Markdown) available via
remark-gfm. - Round-Trip Serialization: Preserves custom elements through MDX syntax during conversion cycles.
Why Use Plate Markdown?
While libraries like react-markdown render Markdown to React elements, @platejs/markdown offers deeper integration with the Plate ecosystem:
- Rich Text Editing: Enables advanced editing features by converting Markdown to Plate's structured format.
- WYSIWYG Experience: Edit content in a rich text view and serialize it back to Markdown.
- Custom Elements & Data: Handles complex custom Plate elements (mentions, embeds) by converting them to/from MDX.
- Extensibility: Leverages Plate's plugin system and the unified/remark ecosystem for powerful customization.
If you only need to display Markdown as HTML without editing or custom
elements, react-markdown might be sufficient. For a rich text editor with
Markdown import/export and custom content, @platejs/markdown is the
integrated solution.
Kit Usage
Installation
The fastest way to add Markdown functionality is with the MarkdownKit, which includes pre-configured MarkdownPlugin with essential remark plugins for Plate UI compatibility.
import {
BaseFootnoteDefinitionPlugin,
BaseFootnoteReferencePlugin,
} from '@platejs/footnote';
import { MarkdownPlugin, remarkMdx, remarkMention } from '@platejs/markdown';
import { KEYS } from 'platejs';
import remarkEmoji from 'remark-emoji';
import remarkGfm from 'remark-gfm';
import remarkMath from 'remark-math';
export const MarkdownKit = [
BaseFootnoteReferencePlugin,
BaseFootnoteDefinitionPlugin,
MarkdownPlugin.configure({
options: {
plainMarks: [KEYS.suggestion, KEYS.comment],
remarkPlugins: [
remarkMath,
remarkGfm,
remarkEmoji as any,
remarkMdx,
remarkMention,
],
},
}),
];import {
BaseFootnoteDefinitionPlugin,
BaseFootnoteReferencePlugin,
} from '@platejs/footnote';
import { MarkdownPlugin, remarkMdx, remarkMention } from '@platejs/markdown';
import { KEYS } from 'platejs';
import remarkEmoji from 'remark-emoji';
import remarkGfm from 'remark-gfm';
import remarkMath from 'remark-math';
export const MarkdownKit = [
BaseFootnoteReferencePlugin,
BaseFootnoteDefinitionPlugin,
MarkdownPlugin.configure({
options: {
plainMarks: [KEYS.suggestion, KEYS.comment],
remarkPlugins: [
remarkMath,
remarkGfm,
remarkEmoji as any,
remarkMdx,
remarkMention,
],
},
}),
];Add Kit
import { createPlateEditor } from 'platejs/react';
import { MarkdownKit } from '@/components/editor/plugins/markdown-kit';
const editor = createPlateEditor({
plugins: [
// ...otherPlugins,
...MarkdownKit,
],
});import { createPlateEditor } from 'platejs/react';
import { MarkdownKit } from '@/components/editor/plugins/markdown-kit';
const editor = createPlateEditor({
plugins: [
// ...otherPlugins,
...MarkdownKit,
],
});Manual Usage
Installation
pnpm add platejs @platejs/markdownpnpm add platejs @platejs/markdownAdd Plugin
import { MarkdownPlugin } from '@platejs/markdown';
import { createPlateEditor } from 'platejs/react';
const editor = createPlateEditor({
plugins: [
// ...otherPlugins,
MarkdownPlugin,
],
});import { MarkdownPlugin } from '@platejs/markdown';
import { createPlateEditor } from 'platejs/react';
const editor = createPlateEditor({
plugins: [
// ...otherPlugins,
MarkdownPlugin,
],
});Configure Plugin
Configuring MarkdownPlugin is recommended to enable Markdown paste handling and set default conversion rules.
import { createPlateEditor } from 'platejs/react';
import {
MarkdownPlugin,
remarkMention,
remarkMdx,
} from '@platejs/markdown';
import remarkEmoji from 'remark-emoji';
import remarkGfm from 'remark-gfm';
import remarkMath from 'remark-math';
const editor = createPlateEditor({
plugins: [
// ...other Plate plugins
MarkdownPlugin.configure({
options: {
// Add remark plugins for syntax extensions (GFM, emoji shortcodes, Math, MDX)
remarkPlugins: [remarkMath, remarkGfm, remarkEmoji, remarkMdx, remarkMention],
// Define custom rules if needed
rules: {
// date: { /* ... rule implementation ... */ },
},
},
}),
],
});
// To disable Markdown paste handling:
const editorWithoutPaste = createPlateEditor({
plugins: [
// ...other Plate plugins
MarkdownPlugin.configure(() => ({ parser: null })),
],
});import { createPlateEditor } from 'platejs/react';
import {
MarkdownPlugin,
remarkMention,
remarkMdx,
} from '@platejs/markdown';
import remarkEmoji from 'remark-emoji';
import remarkGfm from 'remark-gfm';
import remarkMath from 'remark-math';
const editor = createPlateEditor({
plugins: [
// ...other Plate plugins
MarkdownPlugin.configure({
options: {
// Add remark plugins for syntax extensions (GFM, emoji shortcodes, Math, MDX)
remarkPlugins: [remarkMath, remarkGfm, remarkEmoji, remarkMdx, remarkMention],
// Define custom rules if needed
rules: {
// date: { /* ... rule implementation ... */ },
},
},
}),
],
});
// To disable Markdown paste handling:
const editorWithoutPaste = createPlateEditor({
plugins: [
// ...other Plate plugins
MarkdownPlugin.configure(() => ({ parser: null })),
],
});If you don't use MarkdownPlugin with configure, you can still use
editor.api.markdown.deserialize and editor.api.markdown.serialize
directly, but without plugin-configured default rules or paste handling.
Markdown to Plate (Deserialization)
Use editor.api.markdown.deserialize to convert a Markdown string into a Plate Value (an array of nodes). This is often used for the editor's initial content.
import { createPlateEditor } from 'platejs/react';
import { MarkdownPlugin } from '@platejs/markdown';
// ... import other necessary Plate plugins for rendering elements
const markdownString = '# Hello, *Plate*!';
const editor = createPlateEditor({
plugins: [
// MarkdownPlugin must be included
MarkdownPlugin,
// ... other plugins needed to render the deserialized elements (e.g., HeadingPlugin, ItalicPlugin)
],
// Use deserialize in the value factory for initial content
value: (editor) =>
editor.getApi(MarkdownPlugin).markdown.deserialize(markdownString),
});import { createPlateEditor } from 'platejs/react';
import { MarkdownPlugin } from '@platejs/markdown';
// ... import other necessary Plate plugins for rendering elements
const markdownString = '# Hello, *Plate*!';
const editor = createPlateEditor({
plugins: [
// MarkdownPlugin must be included
MarkdownPlugin,
// ... other plugins needed to render the deserialized elements (e.g., HeadingPlugin, ItalicPlugin)
],
// Use deserialize in the value factory for initial content
value: (editor) =>
editor.getApi(MarkdownPlugin).markdown.deserialize(markdownString),
});Ensure all Plate plugins required to render the deserialized Markdown (e.g.,
HeadingPlugin for #, TablePlugin for tables) are included in your
editor's plugins array.
Plate to Markdown (Serialization)
Use editor.api.markdown.serialize to convert the current editor content (or a specific array of nodes) into a Markdown string.
Serializing Current Editor Content:
// Assuming `editor` is your Plate editor instance with content
const markdownOutput = editor.api.markdown.serialize();
console.info(markdownOutput);// Assuming `editor` is your Plate editor instance with content
const markdownOutput = editor.api.markdown.serialize();
console.info(markdownOutput);Serializing Specific Nodes:
const specificNodes = [
{ type: 'p', children: [{ text: 'Serialize just this paragraph.' }] },
{ type: 'h1', children: [{ text: 'And this heading.' }] },
];
// Assuming `editor` is your Plate editor instance
const partialMarkdownOutput = editor.api.markdown.serialize({
value: specificNodes,
});
console.info(partialMarkdownOutput);const specificNodes = [
{ type: 'p', children: [{ text: 'Serialize just this paragraph.' }] },
{ type: 'h1', children: [{ text: 'And this heading.' }] },
];
// Assuming `editor` is your Plate editor instance
const partialMarkdownOutput = editor.api.markdown.serialize({
value: specificNodes,
});
console.info(partialMarkdownOutput);Round-Trip Serialization with Custom Elements (MDX)
A key feature is handling custom Plate elements that lack standard Markdown representation (e.g., underline, mentions). @platejs/markdown converts these to MDX elements during serialization and parses them back during deserialization.
Example: Handling a custom date element.
Plate Node Structure:
{
type: 'p',
children: [
{ text: 'Today is ' },
{ type: 'date', date: '2025-03-31', children: [{ text: '' }] } // Leaf elements need a text child
],
}{
type: 'p',
children: [
{ text: 'Today is ' },
{ type: 'date', date: '2025-03-31', children: [{ text: '' }] } // Leaf elements need a text child
],
}Plugin Configuration with rules:
import type { MdMdxJsxTextElement } from '@platejs/markdown';
import { MarkdownPlugin, remarkMdx } from '@platejs/markdown';
// ... other imports
MarkdownPlugin.configure({
options: {
rules: {
// Key matches:
// 1. Plate element's plugin 'key' or 'type'.
// 2. mdast node type.
// 3. MDX tag name.
date: {
// Markdown -> Plate
deserialize(mdastNode: MdMdxJsxTextElement, deco, options) {
const dateValue = (mdastNode.children?.[0] as any)?.value || '';
return {
type: 'date', // Your Plate element type
date: dateValue,
children: [{ text: '' }], // Valid Plate structure
};
},
// Plate -> Markdown (MDX)
serialize: (slateNode): MdMdxJsxTextElement => {
return {
type: 'mdxJsxTextElement',
name: 'date', // MDX tag name
attributes: [], // Optional: [{ type: 'mdxJsxAttribute', name: 'date', value: slateNode.date }]
children: [{ type: 'text', value: slateNode.date || '1999-01-01' }],
};
},
},
// ... rules for other custom elements
},
remarkPlugins: [remarkMdx /*, ... other remark plugins like remarkGfm */],
},
});import type { MdMdxJsxTextElement } from '@platejs/markdown';
import { MarkdownPlugin, remarkMdx } from '@platejs/markdown';
// ... other imports
MarkdownPlugin.configure({
options: {
rules: {
// Key matches:
// 1. Plate element's plugin 'key' or 'type'.
// 2. mdast node type.
// 3. MDX tag name.
date: {
// Markdown -> Plate
deserialize(mdastNode: MdMdxJsxTextElement, deco, options) {
const dateValue = (mdastNode.children?.[0] as any)?.value || '';
return {
type: 'date', // Your Plate element type
date: dateValue,
children: [{ text: '' }], // Valid Plate structure
};
},
// Plate -> Markdown (MDX)
serialize: (slateNode): MdMdxJsxTextElement => {
return {
type: 'mdxJsxTextElement',
name: 'date', // MDX tag name
attributes: [], // Optional: [{ type: 'mdxJsxAttribute', name: 'date', value: slateNode.date }]
children: [{ type: 'text', value: slateNode.date || '1999-01-01' }],
};
},
},
// ... rules for other custom elements
},
remarkPlugins: [remarkMdx /*, ... other remark plugins like remarkGfm */],
},
});Conversion Process:
- Serialization (Plate → Markdown): The Plate
datenode writes as<date value="2025-03-31" />. - Deserialization (Markdown → Plate): Both
<date value="2025-03-31" />and<date>2025-03-31</date>convert back to the Platedatenode.
API Reference
MarkdownPlugin
The core plugin configuration object. Use MarkdownPlugin.configure({ options: {} }) to set global options for Markdown processing.
Whitelist specific node types (Plate types and Markdown AST types like
strong). Cannot be used with disallowedNodes. If set, only listed
types are processed. Default: null (all allowed).
Blacklist specific node types. Cannot be used with allowedNodes. Listed
types are filtered out. Default: null.
Fine-grained node filtering with custom functions, applied after
allowedNodes/disallowedNodes. - deserialize?: (mdastNode: any) => boolean: Filter for Markdown → Plate. Return true to keep. -
serialize?: (slateNode: any) => boolean: Filter for Plate → Markdown.
Return true to keep. Default: null.
Custom conversion rules between Markdown AST and Plate elements. See
Round-Trip
Serialization and
Customizing Conversion Rules.
For marks/leaves, ensure the rule object has mark: true. Default: null
(uses internal defaultRules).
Array of remark
plugins
(e.g., remark-gfm, remark-math, remark-mdx). Operates on Markdown
AST (mdast). Default: [].
Configuration for pasted content. Set to null to disable Markdown paste
handling. Default enables pasting text/plain as Markdown. See
PlatePlugin API > parser.
api.markdown.deserialize
Converts a Markdown string into a Plate Value (Descendant[]).
Override plugin allowedNodes.
Override plugin disallowedNodes.
Override plugin allowNode.
Adds _memo property with raw Markdown to top-level blocks for
memoization (e.g., with PlateStatic). Default: false.
Override plugin rules.
Options for the underlying Markdown block parser (parseMarkdownBlocks).
See below.
Override plugin remarkPlugins.
If true, single line breaks (\\n) in paragraphs become paragraph
breaks. Default: false.
If true, skips the MDX preprocessing pass and filters remarkMdx out
of the plugin list. Default: false.
Preserves empty paragraph nodes during deserialization.
Receives parser errors before the safe fallback path runs.
api.markdown.deserializeInline
Converts inline Markdown text into Slate children.
api.markdown.serialize
Converts a Plate Value (Descendant[]) into a Markdown string.
Plate nodes to serialize. Defaults to editor.children.
Override plugin allowedNodes.
Override plugin disallowedNodes.
Override plugin allowNode.
Override plugin rules.
Override plugin remarkPlugins (affects stringification).
Options passed to remark-stringify. Defaults to plugin options, with
Plate setting emphasis to _ and resource links to false.
Marks to serialize as plain text instead of Markdown formatting.
Controls spread formatting for list output. Default: false.
Preserves empty paragraph nodes during serialization.
When true, preserves block IDs in markdown serialization to enable AI
comment tracking. Wraps blocks with <block id="...">content</block>
syntax. - Default: false
parseMarkdownBlocks
Utility to parse a Markdown string into block-level tokens (used by deserialize, useful with memoize).
Examples
Using a Remark Plugin (GFM)
Add support for GitHub Flavored Markdown (tables, strikethrough, task lists, autolinks).
Plugin Configuration:
import { createPlateEditor } from 'platejs/react';
import { MarkdownPlugin } from '@platejs/markdown';
import remarkGfm from 'remark-gfm';
// Import Plate plugins for GFM elements
import { TablePlugin } from '@platejs/table/react';
import { TodoListPlugin } from '@platejs/list-classic/react'; // Ensure this is the correct List plugin for tasks
import { StrikethroughPlugin } from '@platejs/basic-nodes/react';
import { LinkPlugin } from '@platejs/link/react';
const editor = createPlateEditor({
plugins: [
// ...other plugins
TablePlugin,
TodoListPlugin, // Or your specific task list plugin
StrikethroughPlugin,
LinkPlugin,
MarkdownPlugin.configure({
options: {
remarkPlugins: [remarkGfm],
},
}),
],
});import { createPlateEditor } from 'platejs/react';
import { MarkdownPlugin } from '@platejs/markdown';
import remarkGfm from 'remark-gfm';
// Import Plate plugins for GFM elements
import { TablePlugin } from '@platejs/table/react';
import { TodoListPlugin } from '@platejs/list-classic/react'; // Ensure this is the correct List plugin for tasks
import { StrikethroughPlugin } from '@platejs/basic-nodes/react';
import { LinkPlugin } from '@platejs/link/react';
const editor = createPlateEditor({
plugins: [
// ...other plugins
TablePlugin,
TodoListPlugin, // Or your specific task list plugin
StrikethroughPlugin,
LinkPlugin,
MarkdownPlugin.configure({
options: {
remarkPlugins: [remarkGfm],
},
}),
],
});Usage:
const markdown = `
A table:
| a | b |
| - | - |
~~Strikethrough~~
- [x] Task list item
Visit https://platejs.org
`;
// Assuming `editor` is your configured Plate editor instance
const slateValue = editor.api.markdown.deserialize(markdown);
// editor.tf.setValue(slateValue); // To set editor content
const markdownOutput = editor.api.markdown.serialize();
// markdownOutput will contain GFM syntaxconst markdown = `
A table:
| a | b |
| - | - |
~~Strikethrough~~
- [x] Task list item
Visit https://platejs.org
`;
// Assuming `editor` is your configured Plate editor instance
const slateValue = editor.api.markdown.deserialize(markdown);
// editor.tf.setValue(slateValue); // To set editor content
const markdownOutput = editor.api.markdown.serialize();
// markdownOutput will contain GFM syntaxCustomizing Rendering (Syntax Highlighting)
This example shows two approaches: customizing the rendering component (common for UI changes) and customizing the conversion rule (advanced, for changing Plate structure).
Background:
@platejs/markdownconverts Markdown fenced code blocks (e.g., ```js ... ```) to Platecode_blockelements withcode_linechildren.- The Plate
CodeBlockElement(often from@platejs/code-block/react) renders this structure. - Syntax highlighting typically occurs within
CodeBlockElementusing a library likelowlight(viaCodeBlockPlugin). See Code Block Plugin for details.
Approach 1: Customizing Rendering Component (Recommended for UI)
To change how code blocks appear, customize the component for the code_block plugin key.
import { createPlateEditor } from 'platejs/react';
import {
CodeBlockPlugin,
CodeLinePlugin,
CodeSyntaxPlugin,
} from '@platejs/code-block/react';
import { MarkdownPlugin } from '@platejs/markdown';
import { MyCustomCodeBlockElement } from './my-custom-code-block'; // Your custom component
const editor = createPlateEditor({
plugins: [
CodeBlockPlugin.withComponent(MyCustomCodeBlockElement), // Base plugin for structure/logic
CodeLinePlugin.withComponent(MyCustomCodeLineElement),
CodeSyntaxPlugin.withComponent(MyCustomCodeSyntaxElement),
MarkdownPlugin, // For Markdown conversion
// ... other plugins
],
});
// MyCustomCodeBlockElement.tsx would then implement the desired rendering
// (e.g., using react-syntax-highlighter), consuming props from PlateElement.import { createPlateEditor } from 'platejs/react';
import {
CodeBlockPlugin,
CodeLinePlugin,
CodeSyntaxPlugin,
} from '@platejs/code-block/react';
import { MarkdownPlugin } from '@platejs/markdown';
import { MyCustomCodeBlockElement } from './my-custom-code-block'; // Your custom component
const editor = createPlateEditor({
plugins: [
CodeBlockPlugin.withComponent(MyCustomCodeBlockElement), // Base plugin for structure/logic
CodeLinePlugin.withComponent(MyCustomCodeLineElement),
CodeSyntaxPlugin.withComponent(MyCustomCodeSyntaxElement),
MarkdownPlugin, // For Markdown conversion
// ... other plugins
],
});
// MyCustomCodeBlockElement.tsx would then implement the desired rendering
// (e.g., using react-syntax-highlighter), consuming props from PlateElement.Refer to the Code Block Plugin documentation for complete examples.
Approach 2: Customizing Conversion Rule (Advanced - Changing Plate Structure)
To fundamentally alter the Plate JSON for code blocks (e.g., storing code as a single string prop), override the deserialize rule.
import { MarkdownPlugin } from '@platejs/markdown';
import { CodeBlockPlugin } from '@platejs/code-block/react';
MarkdownPlugin.configure({
options: {
rules: {
// Override deserialization for mdast 'code' type
code: {
deserialize: (mdastNode, deco, options) => {
return {
type: KEYS.codeBlock, // Use Plate's type
lang: mdastNode.lang ?? undefined,
rawCode: mdastNode.value || '', // Store raw code directly
children: [{ text: '' }], // Plate Element needs a dummy text child
};
},
},
// A custom `serialize` rule for `code_block` would also be needed
// to convert `rawCode` back to an mdast 'code' node.
[KEYS.codeBlock]: {
serialize: (slateNode, options) => {
return {
// mdast 'code' node
type: 'code',
lang: slateNode.lang,
value: slateNode.rawCode,
};
},
},
},
// remarkPlugins: [...]
},
});
// Your custom rendering component (MyCustomCodeBlockElement) would then
// need to read the code from the `rawCode` property.import { MarkdownPlugin } from '@platejs/markdown';
import { CodeBlockPlugin } from '@platejs/code-block/react';
MarkdownPlugin.configure({
options: {
rules: {
// Override deserialization for mdast 'code' type
code: {
deserialize: (mdastNode, deco, options) => {
return {
type: KEYS.codeBlock, // Use Plate's type
lang: mdastNode.lang ?? undefined,
rawCode: mdastNode.value || '', // Store raw code directly
children: [{ text: '' }], // Plate Element needs a dummy text child
};
},
},
// A custom `serialize` rule for `code_block` would also be needed
// to convert `rawCode` back to an mdast 'code' node.
[KEYS.codeBlock]: {
serialize: (slateNode, options) => {
return {
// mdast 'code' node
type: 'code',
lang: slateNode.lang,
value: slateNode.rawCode,
};
},
},
},
// remarkPlugins: [...]
},
});
// Your custom rendering component (MyCustomCodeBlockElement) would then
// need to read the code from the `rawCode` property.Choose based on whether you're changing UI (Approach 1) or data structure (Approach 2).
Using Remark Plugins for Math (remark-math)
Enable TeX math syntax ($inline$, $$block$$).
Plugin Configuration:
import { createPlateEditor } from 'platejs/react';
import { MarkdownPlugin } from '@platejs/markdown';
import remarkMath from 'remark-math';
// Import Plate math plugins for rendering
import { MathPlugin } from '@platejs/math/react'; // Main Math plugin
const editor = createPlateEditor({
plugins: [
// ...other plugins
MathPlugin, // Renders block and inline equations
MarkdownPlugin.configure({
options: {
remarkPlugins: [remarkMath],
// Default rules handle 'math' and 'inlineMath' mdast types from remark-math,
// converting them to Plate's 'equation' and 'inline_equation' types.
},
}),
],
});import { createPlateEditor } from 'platejs/react';
import { MarkdownPlugin } from '@platejs/markdown';
import remarkMath from 'remark-math';
// Import Plate math plugins for rendering
import { MathPlugin } from '@platejs/math/react'; // Main Math plugin
const editor = createPlateEditor({
plugins: [
// ...other plugins
MathPlugin, // Renders block and inline equations
MarkdownPlugin.configure({
options: {
remarkPlugins: [remarkMath],
// Default rules handle 'math' and 'inlineMath' mdast types from remark-math,
// converting them to Plate's 'equation' and 'inline_equation' types.
},
}),
],
});Usage:
const markdown = `
Inline math: $E=mc^2$
Block math:
$$
\\int_a^b f(x) dx = F(b) - F(a)
$$
`;
// Assuming `editor` is your configured Plate editor instance
const slateValue = editor.api.markdown.deserialize(markdown);
// slateValue will contain 'inline_equation' and 'equation' nodes.
const markdownOutput = editor.api.markdown.serialize({ value: slateValue });
// markdownOutput will contain $...$ and $$...$$ syntax.const markdown = `
Inline math: $E=mc^2$
Block math:
$$
\\int_a^b f(x) dx = F(b) - F(a)
$$
`;
// Assuming `editor` is your configured Plate editor instance
const slateValue = editor.api.markdown.deserialize(markdown);
// slateValue will contain 'inline_equation' and 'equation' nodes.
const markdownOutput = editor.api.markdown.serialize({ value: slateValue });
// markdownOutput will contain $...$ and $$...$$ syntax.Using Mentions (remarkMention)
Enable mention syntax using the link format for consistency and special character support.
Plugin Configuration:
import { createPlateEditor } from 'platejs/react';
import { MarkdownPlugin, remarkMention } from '@platejs/markdown';
import { MentionPlugin } from '@platejs/mention/react';
const editor = createPlateEditor({
plugins: [
// ...other plugins
MentionPlugin,
MarkdownPlugin.configure({
options: {
remarkPlugins: [remarkMention],
},
}),
],
});import { createPlateEditor } from 'platejs/react';
import { MarkdownPlugin, remarkMention } from '@platejs/markdown';
import { MentionPlugin } from '@platejs/mention/react';
const editor = createPlateEditor({
plugins: [
// ...other plugins
MentionPlugin,
MarkdownPlugin.configure({
options: {
remarkPlugins: [remarkMention],
},
}),
],
});Supported Format:
const markdown = `
Mention: [Alice](mention:alice)
Mention with spaces: [John Doe](mention:john_doe)
Full name with ID: [Jane Smith](mention:user_123)
`;
// Assuming `editor` is your configured Plate editor instance
const slateValue = editor.api.markdown.deserialize(markdown);
// Creates mention nodes with appropriate values and display text
const markdownOutput = editor.api.markdown.serialize({ value: slateValue });
// All mentions use the link format: [Alice](mention:alice), [John Doe](mention:john_doe), etc.const markdown = `
Mention: [Alice](mention:alice)
Mention with spaces: [John Doe](mention:john_doe)
Full name with ID: [Jane Smith](mention:user_123)
`;
// Assuming `editor` is your configured Plate editor instance
const slateValue = editor.api.markdown.deserialize(markdown);
// Creates mention nodes with appropriate values and display text
const markdownOutput = editor.api.markdown.serialize({ value: slateValue });
// All mentions use the link format: [Alice](mention:alice), [John Doe](mention:john_doe), etc.The remarkMention plugin uses the display text format - a Markdown link-style format that supports spaces and custom display text.
When serializing, all mentions use the link format to ensure consistency and support for special characters.
Using Columns
Enable column layouts with MDX support for multi-column documents.
Plugin Configuration:
import { createPlateEditor } from 'platejs/react';
import { MarkdownPlugin, remarkMdx } from '@platejs/markdown';
import { ColumnPlugin, ColumnItemPlugin } from '@platejs/layout/react';
const editor = createPlateEditor({
plugins: [
// ...other plugins
ColumnPlugin,
ColumnItemPlugin,
MarkdownPlugin.configure({
options: {
remarkPlugins: [remarkMdx], // Required for column MDX syntax
},
}),
],
});import { createPlateEditor } from 'platejs/react';
import { MarkdownPlugin, remarkMdx } from '@platejs/markdown';
import { ColumnPlugin, ColumnItemPlugin } from '@platejs/layout/react';
const editor = createPlateEditor({
plugins: [
// ...other plugins
ColumnPlugin,
ColumnItemPlugin,
MarkdownPlugin.configure({
options: {
remarkPlugins: [remarkMdx], // Required for column MDX syntax
},
}),
],
});Supported Format:
const markdown = `
<column_group>
<column width="50%">
Left column content with 50% width
</column>
<column width="50%">
Right column content with 50% width
</column>
</column_group>
<column_group>
<column width="33%">First</column>
<column width="33%">Second</column>
<column width="34%">Third</column>
</column_group>
`;
// Assuming `editor` is your configured Plate editor instance
const slateValue = editor.api.markdown.deserialize(markdown);
// Creates column_group with nested column elements
const markdownOutput = editor.api.markdown.serialize({ value: slateValue });
// Preserves column structure with width attributesconst markdown = `
<column_group>
<column width="50%">
Left column content with 50% width
</column>
<column width="50%">
Right column content with 50% width
</column>
</column_group>
<column_group>
<column width="33%">First</column>
<column width="33%">Second</column>
<column width="34%">Third</column>
</column_group>
`;
// Assuming `editor` is your configured Plate editor instance
const slateValue = editor.api.markdown.deserialize(markdown);
// Creates column_group with nested column elements
const markdownOutput = editor.api.markdown.serialize({ value: slateValue });
// Preserves column structure with width attributesColumn Features:
- Supports arbitrary number of columns
- Width attributes are optional (defaults to equal distribution)
- Nested content fully supported within columns
- Width normalization ensures columns always sum to 100%
Remark Plugins
@platejs/markdown leverages the unified / remark ecosystem. Extend its capabilities by adding remark plugins via the remarkPlugins option in MarkdownPlugin.configure. These plugins operate on the mdast (Markdown Abstract Syntax Tree).
Finding Plugins:
Common Uses:
- Syntax Extensions:
remark-gfm(tables, etc.),remark-math(TeX),remark-frontmatter,remark-mdx. - Linting/Formatting:
remark-lint(often separate tooling). - Custom Transformations: Custom plugins to modify mdast.
Plate components (e.g., TableElement, CodeBlockElement) render Plate JSON.
remarkPlugins modify the Markdown AST. Unlike some renderers,
rehypePlugins (for HTML AST) are not part of MarkdownPlugin's conversion
pipeline. Run HTML transforms before Plate, or model controlled HTML-like
content as MDX plus explicit rules.
Syntax Support
@platejs/markdown uses remark-parse, adhering to CommonMark. Enable GFM or other syntaxes via remarkPlugins.
- Learn Markdown: CommonMark Help
- GFM Spec: GitHub Flavored Markdown Spec
Architecture Overview
@platejs/markdown bridges Markdown strings and Plate's editor format using the unified/remark ecosystem.
@platejs/markdown
+--------------------------------------------------------------------------------------------+
| |
| +-----------+ +----------------+ +---------------+ +-----------+ |
| | | | | | | | | |
markdown-+->+ remark +-mdast->+ remark plugins +-mdast->+ mdast-to-slate+----->+ nodes +-plate-+->react elements
| | | | | | | | | |
| +-----------+ +----------------+ +---------------+ +-----------+ |
| ^ | |
| | v |
| +-----------+ +----------------+ +---------------+ +-----------+ |
| | | | | | | | | |
| | stringify |<-mdast-+ remark plugins |<-mdast-+ slate-to-mdast+<-----+ serialize | |
| | | | | | | | | |
| +-----------+ +----------------+ +---------------+ +-----------+ |
| |
+--------------------------------------------------------------------------------------------+
Key Steps:
- Parse (Deserialization):
- Markdown string →
remark-parse→ mdast. remarkPluginstransform mdast (e.g.,remark-gfm).mdast-to-slateconverts mdast to Plate nodes usingrules.- Plate renders nodes via its component system.
- Markdown string →
- Stringify (Serialization):
- Plate nodes →
slate-to-mdast(usingrules) → mdast. remarkPluginstransform mdast.remark-stringifyconverts mdast to Markdown string.
- Plate nodes →
- Direct Node Rendering: Plate directly renders its nodes via components,
unlike
react-markdownwhich often uses rehype to convert Markdown to HTML, then to React elements. - Bidirectional: Plate's Markdown processor is fully bidirectional. - Rich Text Integration: Nodes are integrated with Plate's editing capabilities. - Plugin System: Components are managed via Plate's plugin system.
Migrating from react-markdown
Migrating involves mapping react-markdown concepts to Plate's architecture.
Key Differences:
- Rendering Pipeline:
react-markdown(MD → mdast → hast → React) vs.@platejs/markdown(MD ↔ mdast ↔ Plate JSON; Plate components render Plate JSON). - Component Customization:
react-markdown:componentsprop replaces HTML tag renderers.- Plate:
MarkdownPluginrules: Customize mdast ↔ Plate JSON conversion.createPlateEditorcomponents: Customize React components for Plate node types. See Appendix C.
- Plugin Ecosystem:
@platejs/markdownprimarily usesremarkPlugins.rehypePluginsare less common.
Mapping Options:
react-markdown Prop | @platejs/markdown Equivalent/Concept | Notes |
|---|---|---|
children (string) | Pass to editor.api.markdown.deserialize(string) | Input for deserialization; often in createPlateEditor value option. |
remarkPlugins | MarkdownPlugin.configure({ options: { remarkPlugins: [...] }}) | Direct mapping; operates on mdast. |
rehypePlugins | Not part of MarkdownPlugin's conversion pipeline. | Run any HTML pipeline before passing Markdown or Plate nodes to Plate. |
components={{ h1: MyH1 }} | createPlateEditor({ components: { h1: MyH1 } }) | Configures Plate rendering component. Key depends on HeadingPlugin config. |
components={{ code: MyCode }} | 1. Conversion: MarkdownPlugin > rules > code. 2. Rendering: components: { [KEYS.codeBlock]: MyCode } | rules for mdast (code) to Plate (code_block). components for Plate rendering. |
allowedElements | MarkdownPlugin.configure({ options: { allowedNodes: [...] }}) | Filters nodes during conversion (mdast/Plate types). |
disallowedElements | MarkdownPlugin.configure({ options: { disallowedNodes: [...] }}) | Filters nodes during conversion. |
unwrapDisallowed | No direct equivalent. Filtering removes nodes. | Custom rules could implement unwrapping. |
skipHtml | Default behavior strips most HTML. | Sanitize or convert raw HTML before calling editor.api.markdown.deserialize. |
urlTransform | Customize via rules for link (deserialize) or plugin type (serialize). | Handle URL transformations in conversion rules. |
allowElement | MarkdownPlugin.configure({ options: { allowNode: { ... } } }) | Function-based filtering during conversion. |
Appendix A: HTML in Markdown
By default, @platejs/markdown does not process raw HTML tags. Standard Markdown syntax still becomes Plate nodes, but literal HTML like <div> is ignored unless you handle it outside Plate or model it as MDX/custom nodes.
MarkdownPlugin runs remark-parse, configured remarkPlugins, and Plate conversion rules. It does not run a rehype HTML stage, so rehype-raw and rehype-sanitize are not MarkdownPlugin options.
For raw HTML from a trusted source, convert it in your own content pipeline before calling Plate. For untrusted input, sanitize with a strict element and attribute whitelist before deserializing.
const safeMarkdown = await sanitizeMarkdownBeforePlate(untrustedMarkdown);
const value = editor.api.markdown.deserialize(safeMarkdown);const safeMarkdown = await sanitizeMarkdownBeforePlate(untrustedMarkdown);
const value = editor.api.markdown.deserialize(safeMarkdown);For HTML-like custom nodes that you control, prefer MDX syntax with remarkMdx and explicit rules. That keeps the conversion in Plate's supported Markdown pipeline.
Raw HTML can carry XSS payloads. Treat untrusted Markdown as unsafe until your own pipeline sanitizes it with a strict element and attribute whitelist.
Appendix B: Customizing Conversion Rules (rules)
The rules option in MarkdownPlugin.configure offers fine-grained control over mdast ↔ Plate JSON conversion. Keys in the rules object match node types.
- Deserialization (Markdown → Plate): Keys are
mdastnode types (e.g.,paragraph,heading,strong,link, MDX types likemdxJsxTextElement). Thedeserializefunction takes(mdastNode, deco, options)and returns a PlateDescendantorDescendant[]. - Serialization (Plate → Markdown): Keys are Plate element/text types (e.g.,
p,h1,a,code_block,bold). Theserializefunction takes(slateNode, options)and returns anmdastnode.
Example: Overriding Link Deserialization
MarkdownPlugin.configure({
options: {
rules: {
// Rule for mdast 'link' type
link: {
deserialize: (mdastNode, deco, options) => {
// Default creates { type: 'a', url: ..., children: [...] }
// Add a custom property:
return {
type: 'a', // Plate link element type
url: mdastNode.url,
title: mdastNode.title,
customProp: 'added-during-deserialize',
children: convertChildrenDeserialize(
mdastNode.children,
deco,
options
),
};
},
},
// Rule for Plate 'a' type (if serialization needs override for customProp)
a: {
// Assuming 'a' is the Plate type for links
serialize: (slateNode, options) => {
// Default creates mdast 'link'
// Handle customProp if needed in MDX attributes or similar
return {
type: 'link', // mdast type
url: slateNode.url,
title: slateNode.title,
// customProp: slateNode.customProp, // MDX attribute?
children: convertNodesSerialize(slateNode.children, options),
};
},
},
},
// ... remarkPlugins ...
},
});MarkdownPlugin.configure({
options: {
rules: {
// Rule for mdast 'link' type
link: {
deserialize: (mdastNode, deco, options) => {
// Default creates { type: 'a', url: ..., children: [...] }
// Add a custom property:
return {
type: 'a', // Plate link element type
url: mdastNode.url,
title: mdastNode.title,
customProp: 'added-during-deserialize',
children: convertChildrenDeserialize(
mdastNode.children,
deco,
options
),
};
},
},
// Rule for Plate 'a' type (if serialization needs override for customProp)
a: {
// Assuming 'a' is the Plate type for links
serialize: (slateNode, options) => {
// Default creates mdast 'link'
// Handle customProp if needed in MDX attributes or similar
return {
type: 'link', // mdast type
url: slateNode.url,
title: slateNode.title,
// customProp: slateNode.customProp, // MDX attribute?
children: convertNodesSerialize(slateNode.children, options),
};
},
},
},
// ... remarkPlugins ...
},
});Default Rules Summary:
Refer to defaultRules.ts for the complete list. Key conversions include:
| Markdown (mdast) | Plate Type | Notes |
|---|---|---|
paragraph | p | |
heading (depth) | h1 - h6 | Based on depth. |
blockquote | blockquote | |
list (ordered) | ol / p* | ol/li/lic or p with list indent props. |
list (unordered) | ul / p* | ul/li/lic or p with list indent props. |
code (fenced) | code_block | Contains code_line children. |
inlineCode | code (mark) | Applied to text. |
strong | bold (mark) | Applied to text. |
emphasis | italic (mark) | Applied to text. |
delete | strikethrough (mark) | Applied to text. |
link | a | |
image | img | Wraps in paragraph during serialization. |
thematicBreak | hr | |
table | table | Contains tr. |
math (block) | equation | Requires remark-math. |
inlineMath | inline_equation | Requires remark-math. |
mdxJsxFlowElement | Custom | Requires remark-mdx and custom rules. |
mdxJsxTextElement | Custom | Requires remark-mdx and custom rules. |
* List conversion depends on ListPlugin detection.
Emoji shortcodes: Add remark-emoji to remarkPlugins to turn :fire: into unicode 🔥 on deserialization and back to unicode on serialization.
GFM footnotes: With remark-gfm enabled, footnotes deserialize into footnoteReference and footnoteDefinition nodes. Add the matching Footnote plugins to render them as real editor nodes instead of falling back to unknown types.
Default MDX Conversions (with remark-mdx):
| MDX (mdast) | Plate Type | Notes |
|---|---|---|
<del>...</del> | strikethrough (mark) | Alt for ~~strikethrough~~ |
<sub>...</sub> | subscript (mark) | H2O |
<sup>...</sup> | superscript (mark) | E=mc2 |
<u>...</u> | underline (mark) | Underlined |
<mark>...</mark> | highlight (mark) | Highlighted |
<span style="font-family: ..."> | fontFamily (mark) | |
<span style="font-size: ..."> | fontSize (mark) | |
<span style="font-weight: ..."> | fontWeight (mark) | |
<span style="color: ..."> | color (mark) | |
<span style="background-color: ..."> | backgroundColor (mark) | |
<date>...</date> | date | Custom Date element |
[text](mention:id) | mention | Custom Mention element |
<file name="..." /> | file | Custom File element |
<audio src="..." /> | audio | Custom Audio element |
<video src="..." /> | video | Custom Video element |
<toc /> | toc | Table of Contents |
<callout>...</callout> | callout | Callout block |
<column_group>...</column_group> | column_group | Multi-column layout container |
<column width="50%">...</column> | column | Single column with optional width attribute |
Appendix C: Components for Rendering
While rules handle MD ↔ Plate conversion, Plate uses React components to render Plate nodes. Configure these in createPlateEditor via the components option or plugin withComponent method.
Example:
import { createPlateEditor, ParagraphPlugin, PlateLeaf } from 'platejs/react';
import { BoldPlugin } from '@platejs/basic-nodes/react';
import { CodeBlockPlugin } from '@platejs/code-block/react';
import { ParagraphElement } from '@/components/ui/paragraph-node'; // Example UI component
import { CodeBlockElement } from '@/components/ui/code-block-node'; // Example UI component
const editor = createPlateEditor({
plugins: [
ParagraphPlugin.withComponent(ParagraphElement),
CodeBlockPlugin.withComponent(CodeBlockElement),
BoldPlugin,
/* ... */
],
});import { createPlateEditor, ParagraphPlugin, PlateLeaf } from 'platejs/react';
import { BoldPlugin } from '@platejs/basic-nodes/react';
import { CodeBlockPlugin } from '@platejs/code-block/react';
import { ParagraphElement } from '@/components/ui/paragraph-node'; // Example UI component
import { CodeBlockElement } from '@/components/ui/code-block-node'; // Example UI component
const editor = createPlateEditor({
plugins: [
ParagraphPlugin.withComponent(ParagraphElement),
CodeBlockPlugin.withComponent(CodeBlockElement),
BoldPlugin,
/* ... */
],
});Refer to Plugin Components for more on creating/registering components.
Appendix D: PlateMarkdown Component (Read-Only Display)
For a react-markdown-like component for read-only display:
import React, { useEffect } from 'react';
import { Plate, PlateContent, usePlateEditor } from 'platejs/react';
import { MarkdownPlugin } from '@platejs/markdown';
// Import necessary Plate plugins for common Markdown features
import { HeadingPlugin } from '@platejs/basic-nodes/react';
// ... include other plugins like BlockquotePlugin, CodeBlockPlugin, ListPlugin, etc.
// ... and mark plugins like BoldPlugin, ItalicPlugin, etc.
export interface PlateMarkdownProps {
children: string; // Markdown content
remarkPlugins?: any[];
components?: Record<string, React.ComponentType<any>>; // Plate component overrides
className?: string;
}
export function PlateMarkdown({
children,
remarkPlugins = [],
components = {},
className,
}: PlateMarkdownProps) {
const editor = usePlateEditor({
plugins: [
// Include all plugins needed to render your Markdown
HeadingPlugin /* ... other plugins ... */,
MarkdownPlugin.configure({ options: { remarkPlugins } }),
],
components, // Pass through component overrides
});
useEffect(() => {
editor.tf.reset(); // Clear previous content
editor.tf.setValue(
editor.getApi(MarkdownPlugin).markdown.deserialize(children)
);
}, [children, editor, remarkPlugins]); // Re-deserialize if markdown or plugins change
return (
<Plate editor={editor}>
<PlateContent readOnly className={className} />
</Plate>
);
}
// Usage Example:
// const markdownString = "# Hello\nThis is *Markdown*.";
// <PlateMarkdown className="prose dark:prose-invert">
// {markdownString}
// </PlateMarkdown>import React, { useEffect } from 'react';
import { Plate, PlateContent, usePlateEditor } from 'platejs/react';
import { MarkdownPlugin } from '@platejs/markdown';
// Import necessary Plate plugins for common Markdown features
import { HeadingPlugin } from '@platejs/basic-nodes/react';
// ... include other plugins like BlockquotePlugin, CodeBlockPlugin, ListPlugin, etc.
// ... and mark plugins like BoldPlugin, ItalicPlugin, etc.
export interface PlateMarkdownProps {
children: string; // Markdown content
remarkPlugins?: any[];
components?: Record<string, React.ComponentType<any>>; // Plate component overrides
className?: string;
}
export function PlateMarkdown({
children,
remarkPlugins = [],
components = {},
className,
}: PlateMarkdownProps) {
const editor = usePlateEditor({
plugins: [
// Include all plugins needed to render your Markdown
HeadingPlugin /* ... other plugins ... */,
MarkdownPlugin.configure({ options: { remarkPlugins } }),
],
components, // Pass through component overrides
});
useEffect(() => {
editor.tf.reset(); // Clear previous content
editor.tf.setValue(
editor.getApi(MarkdownPlugin).markdown.deserialize(children)
);
}, [children, editor, remarkPlugins]); // Re-deserialize if markdown or plugins change
return (
<Plate editor={editor}>
<PlateContent readOnly className={className} />
</Plate>
);
}
// Usage Example:
// const markdownString = "# Hello\nThis is *Markdown*.";
// <PlateMarkdown className="prose dark:prose-invert">
// {markdownString}
// </PlateMarkdown>This PlateMarkdown component provides a read-only view. For full
editing, see the Installation guides.
Security Considerations
@platejs/markdown prioritizes safety by converting Markdown to a structured Plate format, avoiding direct HTML rendering. However, security depends on:
- Custom
rules: Ensuredeserializerules don't introduce unsafe data. remarkPlugins: Vet third-party remark plugins for potential security risks.- Raw HTML Processing: Sanitize or convert raw HTML before passing Markdown to Plate. Treat untrusted Markdown as unsafe until your own pipeline has applied a strict whitelist.
- Plugin Responsibility: URL validation in
LinkPlugin(isUrl) orMediaEmbedPlugin(parseMediaUrl) is crucial.
Recommendation: Treat untrusted Markdown input cautiously. Sanitize if allowing complex features or raw HTML.
Related Links
- remark: Markdown processor.
- unified: Core processing engine.
- MDX: JSX in Markdown.
- react-markdown: Alternative React Markdown component.
- remark-slate-transformer: Initial mdast ↔ Plate conversion work by inokawa.
On This Page
FeaturesWhy Use Plate Markdown?Kit UsageInstallationAdd KitManual UsageInstallationAdd PluginConfigure PluginMarkdown to Plate (Deserialization)Plate to Markdown (Serialization)Round-Trip Serialization with Custom Elements (MDX)API ReferenceMarkdownPluginapi.markdown.deserializeapi.markdown.deserializeInlineapi.markdown.serializeparseMarkdownBlocksExamplesUsing a Remark Plugin (GFM)Customizing Rendering (Syntax Highlighting)Using Remark Plugins for Math (remark-math)Using Mentions (remarkMention)Using ColumnsRemark PluginsSyntax SupportArchitecture OverviewMigrating from react-markdownAppendix A: HTML in MarkdownAppendix B: Customizing Conversion Rules (rules)Appendix C: Components for RenderingAppendix D: PlateMarkdown Component (Read-Only Display)Security ConsiderationsRelated Links