This guide covers converting Plate editor content to HTML (serializeHtml
) and parsing HTML back into Plate's format (editor.api.html.deserialize
).
'use client';
import * as React from 'react';
import { Plate, usePlateEditor } from 'platejs/react';
import { EditorKit } from '@/components/editor/editor-kit';
import { Editor, EditorContainer } from '@/components/ui/editor';
import { DEMO_VALUES } from './values/demo-values';
export default function Demo({ id }: { id: string }) {
const editor = usePlateEditor({
plugins: EditorKit,
value: DEMO_VALUES[id],
});
return (
<Plate editor={editor}>
<EditorContainer variant="demo">
<Editor />
</EditorContainer>
</Plate>
);
}
Kit Usage
Installation
The fastest way to enable HTML serialization is with the BaseEditorKit
, which includes pre-configured base plugins that support HTML conversion for most common elements and marks.
import { BaseAlignKit } from './plugins/align-base-kit';
import { BaseBasicBlocksKit } from './plugins/basic-blocks-base-kit';
import { BaseBasicMarksKit } from './plugins/basic-marks-base-kit';
import { BaseCalloutKit } from './plugins/callout-base-kit';
import { BaseCodeBlockKit } from './plugins/code-block-base-kit';
import { BaseColumnKit } from './plugins/column-base-kit';
import { BaseCommentKit } from './plugins/comment-base-kit';
import { BaseDateKit } from './plugins/date-base-kit';
import { BaseFontKit } from './plugins/font-base-kit';
import { BaseLineHeightKit } from './plugins/line-height-base-kit';
import { BaseLinkKit } from './plugins/link-base-kit';
import { BaseListKit } from './plugins/list-base-kit';
import { MarkdownKit } from './plugins/markdown-kit';
import { BaseMathKit } from './plugins/math-base-kit';
import { BaseMediaKit } from './plugins/media-base-kit';
import { BaseMentionKit } from './plugins/mention-base-kit';
import { BaseSuggestionKit } from './plugins/suggestion-base-kit';
import { BaseTableKit } from './plugins/table-base-kit';
import { BaseTocKit } from './plugins/toc-base-kit';
import { BaseToggleKit } from './plugins/toggle-base-kit';
export const BaseEditorKit = [
...BaseBasicBlocksKit,
...BaseCodeBlockKit,
...BaseTableKit,
...BaseToggleKit,
...BaseTocKit,
...BaseMediaKit,
...BaseCalloutKit,
...BaseColumnKit,
...BaseMathKit,
...BaseDateKit,
...BaseLinkKit,
...BaseMentionKit,
...BaseBasicMarksKit,
...BaseFontKit,
...BaseListKit,
...BaseAlignKit,
...BaseLineHeightKit,
...BaseCommentKit,
...BaseSuggestionKit,
...MarkdownKit,
];
Add Kit
import { createSlateEditor, serializeHtml } from 'platejs';
import { BaseEditorKit } from '@/components/editor/editor-base-kit';
const editor = createSlateEditor({
plugins: BaseEditorKit,
value: [
{ type: 'h1', children: [{ text: 'Hello World' }] },
{ type: 'p', children: [{ text: 'This content will be serialized to HTML.' }] },
],
});
// Serialize to HTML
const html = await serializeHtml(editor);
Example
See a complete server-side HTML generation example:
import * as React from 'react';
import { cva } from 'class-variance-authority';
import fs from 'node:fs/promises';
import path from 'node:path';
import { type Value, createSlateEditor, serializeHtml } from 'platejs';
import { BaseEditorKit } from '@/components/editor/editor-base-kit';
import {
EditorClient,
ExportHtmlButton,
HtmlIframe,
} from '@/components/editor/slate-to-html';
import { alignValue } from '@/registry/examples/values/align-value';
import { basicBlocksValue } from '@/registry/examples/values/basic-blocks-value';
import { basicMarksValue } from '@/registry/examples/values/basic-marks-value';
import { columnValue } from '@/registry/examples/values/column-value';
import { dateValue } from '@/registry/examples/values/date-value';
import { discussionValue } from '@/registry/examples/values/discussion-value';
import { equationValue } from '@/registry/examples/values/equation-value';
import { fontValue } from '@/registry/examples/values/font-value';
import { indentValue } from '@/registry/examples/values/indent-value';
import { lineHeightValue } from '@/registry/examples/values/line-height-value';
import { linkValue } from '@/registry/examples/values/link-value';
import { listValue } from '@/registry/examples/values/list-value';
import { mediaValue } from '@/registry/examples/values/media-value';
import { mentionValue } from '@/registry/examples/values/mention-value';
import { tableValue } from '@/registry/examples/values/table-value';
import { tocPlaygroundValue } from '@/registry/examples/values/toc-value';
import { createHtmlDocument } from '@/lib/create-html-document';
import { EditorStatic } from '@/components/ui/editor-static';
const getCachedTailwindCss = React.cache(async () => {
const cssPath = path.join(process.cwd(), 'public', 'tailwind.css');
return await fs.readFile(cssPath, 'utf8');
});
export default async function SlateToHtmlBlock() {
const createValue = (): Value => [
...basicBlocksValue,
...basicMarksValue,
...tocPlaygroundValue,
...linkValue,
...tableValue,
...equationValue,
...columnValue,
...mentionValue,
...dateValue,
...fontValue,
...discussionValue,
...alignValue,
...lineHeightValue,
...indentValue,
...listValue,
...mediaValue,
];
const editor = createSlateEditor({
plugins: BaseEditorKit,
value: createValue(),
});
const tailwindCss = await getCachedTailwindCss();
const katexCDN = `<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/katex.css" integrity="sha384-9PvLvaiSKCPkFKB1ZsEoTjgnJn+O3KvEwtsz37/XrkYft3DTk2gHdYvd9oWgW3tV" crossorigin="anonymous">`;
// const cookieStore = await cookies();
// const theme = cookieStore.get('theme')?.value;
const theme = 'light';
// Get the editor content HTML using EditorStatic
const editorHtml = await serializeHtml(editor, {
editorComponent: EditorStatic,
props: { style: { padding: '0 calc(50% - 350px)', paddingBottom: '' } },
});
// Create the full HTML document
const html = createHtmlDocument({
editorHtml,
katexCDN,
tailwindCss,
theme,
});
return (
<div className="grid grid-cols-3 px-4">
<div className="p-2">
<h3 className={headingVariants()}>Editor</h3>
<EditorClient value={createValue()} />
</div>
<div className="p-2">
<h3 className={headingVariants()}>EditorStatic</h3>
<EditorStatic editor={editor} />
</div>
<div className="relative p-2">
<h3 className={headingVariants()}>HTML Iframe</h3>
<ExportHtmlButton
className="absolute top-10 right-0"
html={html}
serverTheme={theme}
/>
<HtmlIframe
className="h-[7500px] w-full"
html={html}
serverTheme={theme}
/>
</div>
</div>
);
}
const headingVariants = cva(
'group mt-8 scroll-m-20 font-heading text-xl font-semibold tracking-tight'
);
Plate to HTML
Convert Plate editor content (Plate nodes) into an HTML string. This is often done server-side.
Key Server-Side Constraint
When using serializeHtml
or other Plate utilities in a server environment (Node.js, RSC), you must not import from /react
subpaths of any platejs*
package. Always use the base imports (e.g., @platejs/basic-nodes
instead of @platejs/basic-nodes/react
).
This means you should use createSlateEditor
from platejs
for server-side editor instances, not usePlateEditor
or createPlateEditor
from platejs/react
.
Basic Usage
Provide a server-side editor instance and configure your Plate components during editor creation.
import { createSlateEditor, serializeHtml } from 'platejs'; // Base import
// Import base plugins (NOT from /react paths)
import { BaseHeadingPlugin } from '@platejs/basic-nodes';
// Import your STATIC components for rendering
import { ParagraphElementStatic } from '@/components/ui/paragraph-node-static';
import { HeadingElementStatic } from '@/components/ui/heading-node-static';
// For a styled static output, you might use a wrapper like EditorStatic
import { EditorStatic } from '@/components/ui/editor-static';
// Map plugin keys to their STATIC rendering components
const components = {
p: ParagraphElementStatic, // 'p' is the default key for paragraphs
h1: HeadingElementStatic,
// ... add mappings for all your elements and marks
};
// Create a server-side editor instance with components
const editor = createSlateEditor({
plugins: [
BaseHeadingPlugin, // Base plugin for headings
// ... add all other base plugins relevant to your content
],
components,
});
async function getMyHtml() {
// Example: set some content on the server-side editor
editor.children = [
{ type: 'h1', children: [{text: 'My Title'}] },
{ type: 'p', children: [{text: 'My content.'}] }
];
const html = await serializeHtml(editor, {
// Optional: Use a custom wrapper like EditorStatic for styling
// editorComponent: EditorStatic,
// props: { variant: 'none', className: 'p-4 m-4 border' },
});
return html;
}
Styling Serialized HTML
serializeHtml
returns only the HTML for the editor content itself. If you use styled components (like EditorStatic
or custom static components with specific classes), you must ensure the necessary CSS is available in the final context where the HTML will be displayed.
This often means wrapping the serialized HTML in a full HTML document that includes your stylesheets:
// ... (previous setup from generate-html.ts)
async function getFullHtmlDocument() {
const editorHtmlContent = await getMyHtml(); // From previous example
const fullHtml = `<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/path/to/your-global-styles.css" />
<link rel="stylesheet" href="/path/to/tailwind-or-component-styles.css" />
<title>Serialized Content</title>
</head>
<body>
<div class="my-document-wrapper prose dark:prose-invert">
${editorHtmlContent}
</div>
</body>
</html>`;
return fullHtml;
}
Static Output Only
The serialization process converts Plate nodes to static HTML. Interactive features (React event handlers, client-side hooks) or components relying on browser APIs will not function in the serialized output.
Using Static Components
For server-side serialization, you must use static versions of your components (no client-only code, no React hooks like useEffect
or useState
).
Refer to the Static Rendering Guide for detailed instructions on creating server-safe static components for your Plate elements and marks.
import React from 'react';
import type { SlateElementProps } from 'platejs';
// Example static paragraph component
export function ParagraphElementStatic(props: SlateElementProps) {
return (
<SlateElement {...props} className={cn('m-0 px-0 py-1')}>
{props.children}
</SlateElement>
);
}
HTML to Plate
The HTML deserializer allows you to convert HTML content (strings or DOM elements) back into Plate format. This supports round-trip conversion, preserving structure, formatting, and attributes where corresponding plugin rules exist.
Basic Usage
Use editor.api.html.deserialize
within a client-side Plate editor context.
import { PlateEditor, usePlateEditor } from 'platejs/react'; // React-specific imports for client-side
// Import ALL Plate plugins needed to represent the HTML content
import { HeadingPlugin } from '@platejs/basic-nodes/react';
// ... and so on for bold, italic, tables, lists, etc.
function MyHtmlImporter({ htmlString }: { htmlString: string }) {
const editor = usePlateEditor({
plugins: [
HeadingPlugin, // For <h1>, <h2>, etc.
// ... include all plugins corresponding to the HTML you expect to parse
],
});
const handleImport = () => {
const slateValue = editor.api.html.deserialize(htmlString);
editor.tf.setValue(slateValue);
};
// ... render your editor and a button to trigger handleImport ...
return <button onClick={handleImport}>Import HTML</button>;
}
Client-Side Operation
HTML deserialization using editor.api.html.deserialize
is typically a client-side operation as it interacts with a live Plate editor instance configured with React components and plugins.
Plugin Deserialization Rules Overview
Each Plate plugin can define rules for how it interprets specific HTML tags, styles, and attributes during deserialization. Below is a summary of common HTML structures and the Plate plugins typically responsible for them.
HTML Element / Style | Plate Plugin (Typical) | Notes |
---|---|---|
<strong> , <b> , font-weight: 600,700,bold | BoldPlugin | Converts to bold: true mark. |
<em> , <i> , font-style: italic | ItalicPlugin | Converts to italic: true mark. |
<u> , text-decoration: underline | UnderlinePlugin | Converts to underline: true mark. |
<s> , <del> , <strike> , text-decoration: line-through | StrikethroughPlugin | Converts to strikethrough: true mark. |
<sub> , vertical-align: sub | SubscriptPlugin | Converts to subscript: true mark. |
<sup> , vertical-align: super | SuperscriptPlugin | Converts to superscript: true mark. |
<code> (not in <pre> ), font-family: Consolas | CodePlugin | Converts to code: true mark (inline code). |
<kbd> | KbdPlugin | Converts to kbd: true mark. |
<p> | ParagraphPlugin | Converts to paragraph element. |
<h1> - <h6> | HeadingPlugin | Converts to corresponding heading elements (h1 - h6 ). |
<ul> | ListPlugin (classic) | Converts to unordered list (ul type). Items become li . |
<ol> | ListPlugin (classic) | Converts to ordered list (ol type). Items become li . |
<li> (within <ul> or <ol> ) | ListPlugin (classic) | Converts to list item (li type), with lic (list item content) child. |
<li> (with aria-level for indent) | ListPlugin | Converts to paragraph with indent and listStyleType props. |
<blockquote> | BlockquotePlugin | Converts to blockquote element. |
<pre> (often with <code> inside) | CodeBlockPlugin | Converts to code_block element. Content split into code_line . |
<hr> | HorizontalRulePlugin | Converts to horizontal rule element. |
<a> | LinkPlugin | Converts to link element (a type) with url property. |
<img> | ImagePlugin | Converts to image element (img type) with url property. |
<iframe> | MediaEmbedPlugin | Converts to media embed element, attempting to parse URL. |
<table> | TablePlugin | Converts to table element. |
<tr> | TablePlugin | Converts to tr (table row) element. |
<td> | TablePlugin | Converts to td (table cell) element. |
<th> | TablePlugin | Converts to th (table header cell) element. |
style="background-color: ..." | FontColorPlugin | Converts to backgroundColor mark. (Plugin name might seem inverse) |
style="color: ..." | FontColorPlugin | Converts to color mark. |
style="font-family: ..." | FontFamilyPlugin | Converts to fontFamily mark. |
style="font-size: ..." | FontSizePlugin | Converts to fontSize mark. |
style="font-weight: ..." (other than bold values) | FontWeightPlugin | Converts to fontWeight mark for non-standard bold values. |
<mark> | HighlightPlugin | Converts to highlight: true mark. |
style="text-align: ..." | TextAlignPlugin | Sets align property on block elements. |
style="line-height: ..." | LineHeightPlugin | Sets lineHeight property on block elements. |
Plugin Configuration
The exact Plate type (e.g., ParagraphPlugin.key
vs. 'p'
) depends on how plugins are configured. The table shows typical associations. Ensure the corresponding Plate plugins are included in your editor for these rules to apply.
Deserialization Properties in Plugins
Plugins can define how they handle HTML deserialization using properties within their parsers.html.deserializer
configuration:
parse
: A function({ editor, element, getOptions, ... }) => Partial<SlateNode>
that takes an HTML element and returns a partial Plate node. This is where the main conversion logic resides.query
: An optional function({ element, getOptions }) => boolean
that determines if the deserializer rule should even be considered for the current HTML element.rules
: An array of rule objects, each defining conditions for matching an HTML element:validNodeName
: String or array of strings for matching HTML tag names (e.g.,'P'
,['STRONG', 'B']
).validAttribute
: Object or array of objects specifying required attribute names and/or values (e.g.,{ align: ['left', 'center'] }
).validClassName
: String or array of strings for matching CSS class names.validStyle
: Object or array of objects specifying required CSS style properties and/or values (e.g.,{ fontWeight: ['600', '700', 'bold'] }
).
isElement
: Boolean,true
if the plugin deserializes an HTML element into a Plate Element node.isLeaf
: Boolean,true
if the plugin deserializes an HTML element or style into a Plate Leaf (mark) on a Text node.attributeNames
: Array of HTML attribute names whose values should be preserved on thenode.attributes
property of the resulting Plate node.withoutChildren
: Boolean, iftrue
, child nodes of the HTML element are not processed byconvertHtmlAttributes
.
Customizing Deserialization Behavior
You can extend a plugin to modify its HTML parsing logic. This is useful for supporting non-standard HTML attributes or structures.
import { CodeBlockPlugin } from '@platejs/code-block/react';
import { CodeLinePlugin } from '@platejs/code-block'; // Base if needed
const MyCustomCodeBlockPlugin = CodeBlockPlugin.configure({
parsers: {
html: {
deserializer: {
// Inherit most rules and properties, then override or add
...CodeBlockPlugin.parsers.html.deserializer,
parse: ({ element, editor }) => { // editor might be needed for getType
const language = element.getAttribute('data-custom-lang') || element.className.match(/language-(?<lang>[^\s]+)/)?.groups?.lang;
const textContent = element.textContent || '';
const lines = textContent.split('\n');
return {
type: CodeBlockPlugin.key, // Or editor.getType(CodeBlockPlugin.key)
lang: language,
code: textContent, // Example: store full code string
children: lines.map((line) => ({
type: editor.getType(CodeLinePlugin.key),
children: [{ text: line }],
})),
};
},
rules: [
// Inherit existing rules if desired
...(CodeBlockPlugin.parsers.html.deserializer.rules || []),
// Add a new rule to match based on a custom attribute
{ validAttribute: { 'data-custom-lang': true } },
],
},
},
},
});
// Then use MyCustomCodeBlockPlugin in your editor configuration.
This example customizes CodeBlockPlugin
to look for a data-custom-lang
attribute or a language-*
class for determining the code language.
Advanced Deserialization Example (ListPlugin
)
The ListPlugin
demonstrates a more complex deserialization scenario where it transforms HTML list structures (<li>
elements) into indented paragraphs within Plate, using aria-level
to determine indentation.
Here's a conceptual look at its deserialization logic:
// Simplified concept from ListPlugin
export const ListPluginConfig = {
// ... other configurations ...
parsers: {
html: {
deserializer: {
isElement: true,
// query: ({ element }) => hasListAncestor(element), // Example condition
parse: ({ editor, element }) => ({
type: 'p', // Converts <li> to <p>
indent: Number(element.getAttribute('aria-level') || '1'),
listStyleType: element.style.listStyleType || undefined,
// Children are processed by Plate's default deserializer after this node is created
}),
rules: [
{ validNodeName: 'LI' }, // Only applies to <li> elements
],
},
},
},
};
This illustrates how a plugin can completely reinterpret HTML structures into a different Plate representation.
API Reference
serializeHtml(editor, options)
Converts Plate nodes from editor.children
(or a provided value
) into an HTML string. This function is typically used server-side.
A React component to wrap the entire editor content during static rendering. Defaults to PlateStatic
.
The component receives editor
, value
, and any props
passed here.
Props to pass to the editorComponent
. P
defaults to PlateStaticProps
.
Plate nodes to serialize. If not provided, editor.children
will be used.
An array of class name prefixes to preserve if stripClassNames
is true. null
preserves all if stripClassNames
is true. Default: ['slate-', 'line-clamp']
.
If true
, removes all class names from the output HTML except those whose prefixes are listed in preserveClassNames
. Default: true
.
If true
, removes all data-*
attributes from the output HTML. Default: true
.
api.html.deserialize(options)
Parses an HTML string or HTMLElement
into a Plate Value
(an array of Descendant
nodes). This is typically used on the client-side with a fully configured Plate editor.
The HTML string or HTMLElement
to deserialize.
If true
(default), collapses whitespace from text nodes similarly to how browsers treat whitespace in HTML. Set to false
to preserve all whitespace. Default: true
.
Deprecated. Use collapseWhiteSpace
. If true
, leading/trailing whitespace is trimmed and sequences of whitespace are collapsed. Default: true
.
Next Steps
- Explore the Static Rendering guide for creating server-safe components.
- Review individual plugin documentation for specific HTML serialization/deserialization capabilities and default rules.
- See the Server-Side HTML Generation Example.
On This Page
Kit UsageInstallationAdd KitExamplePlate to HTMLBasic UsageStyling Serialized HTMLUsing Static ComponentsHTML to PlateBasic UsagePlugin Deserialization Rules OverviewDeserialization Properties in PluginsCustomizing Deserialization BehaviorAdvanced Deserialization Example (ListPlugin)API ReferenceserializeHtml(editor, options)api.html.deserialize(options)Next Steps