Use renderStaticHtml to render a configured static Plate tree to HTML. Use
editor.api.html.deserialize(...) to parse HTML into Plate
content. Static React rendering and semantic HTML parsing have different
owners, so this guide treats each direction separately.
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 './align-static';import { BaseBasicBlocksKit } from './basic-blocks-static';import { BaseBasicMarksKit } from './basic-marks-static';import { BaseCalloutKit } from './callout-static';import { BaseCodeBlockKit } from './code-block-static';import { BaseColumnKit } from './column-static';import { BaseCommentKit } from './comment-static';import { BaseDateKit } from './date-static';import { BaseFontKit } from './font-static';import { BaseFootnoteKit } from './footnote-static';import { BaseLineHeightKit } from './line-height-static';import { BaseLinkKit } from './link-static';import { BaseListKit } from './list-static';import { MarkdownKit } from './markdown';import { BaseMathKit } from './math-static';import { BaseMediaKit } from './media-static';import { BaseMentionKit } from './mention-static';import { BaseSuggestionKit } from './suggestion-static';import { BaseTableKit } from './table-static';import { BaseTocKit } from './toc-static';import { BaseToggleKit } from './toggle-static';export const BaseEditorKit = [ ...BaseBasicBlocksKit, ...BaseCodeBlockKit, ...BaseTableKit, ...BaseToggleKit, ...BaseTocKit, ...BaseMediaKit, ...BaseCalloutKit, ...BaseColumnKit, ...BaseMathKit, ...BaseDateKit, ...BaseLinkKit, ...BaseMentionKit, ...BaseFootnoteKit, ...BaseBasicMarksKit, ...BaseFontKit, ...BaseListKit, ...BaseAlignKit, ...BaseLineHeightKit, ...BaseCommentKit, ...BaseSuggestionKit, ...MarkdownKit,] as const;
import { BaseAlignKit } from './align-static';import { BaseBasicBlocksKit } from './basic-blocks-static';import { BaseBasicMarksKit } from './basic-marks-static';import { BaseCalloutKit } from './callout-static';import { BaseCodeBlockKit } from './code-block-static';import { BaseColumnKit } from './column-static';import { BaseCommentKit } from './comment-static';import { BaseDateKit } from './date-static';import { BaseFontKit } from './font-static';import { BaseFootnoteKit } from './footnote-static';import { BaseLineHeightKit } from './line-height-static';
Add Kit
import { createBaseEditor } from 'platejs';import { renderStaticHtml } from 'platejs/static';import { BaseEditorKit } from '@/components/editor/plugins-static';const editor = createBaseEditor({ plugins: BaseEditorKit, initialValue: [ { type: 'heading', level: 1, children: [{ text: 'Hello World' }] }, { type: 'paragraph', children: [{ text: 'This content will be serialized to HTML.' }] }, ],});// Serialize to HTMLconst html = await renderStaticHtml(editor);
Example
See a complete server-side HTML generation example:
import fs from 'node:fs/promises';import path from 'node:path';import { cva } from 'class-variance-authority';import type { Metadata } from 'next';import type { EditorDocumentValue } from 'platejs';import { createStaticEditor, renderStaticHtml } from 'platejs/static';import * as React from 'react';import { EditorStatic } from '@/components/editor/editor-static';import { EditorClient, EditorViewClient, ExportHtmlButton, HtmlIframe,
Plate to HTML
Convert Plate editor content (Plate nodes) into an HTML string. This is often done server-side.
When using renderStaticHtml 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 createBaseEditor 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.
lib/generate-html.ts
import { createBaseEditor } from 'platejs';import { renderStaticHtml } from 'platejs/static'; // Static import// Import a server-safe registry kit (NOT from /react package paths)import { BaseBasicBlocksKit } from '@/components/editor/basic-blocks-static';// Import your STATIC components for renderingimport { ParagraphElementStatic } from '@/components/editor/paragraph-static';import { HeadingElementStatic } from '@/components/editor/heading-static';// For a styled static output, you might use a wrapper like EditorStaticimport { EditorStatic } from '@/components/editor/editor-static';// Map plugin names to their STATIC rendering componentsconst components = { p: ParagraphElementStatic, // 'p' is the default name for paragraphs h1: HeadingElementStatic, // ... add mappings for all your elements and marks};// Create a server-side editor instance with componentsconst editor = createBaseEditor({ plugins: [ ...BaseBasicBlocksKit, // Paragraph, headings, blockquote, and horizontal rule // ... add all other base plugins relevant to your content ], components, initialValue: [ { type: 'heading', level: 1, children: [{ text: 'My Title' }] }, { type: 'paragraph', children: [{ text: 'My content.' }] }, ],});async function getMyHtml() { const html = await renderStaticHtml(editor, { // Optional: Use a custom wrapper like EditorStatic for styling // editorComponent: EditorStatic, // props: { variant: 'none', className: 'p-4 m-4 border' }, }); return html;}
lib/generate-html.ts
import { createBaseEditor } from 'platejs';import { renderStaticHtml } from 'platejs/static'; // Static import// Import a server-safe registry kit (NOT from /react package paths)import { BaseBasicBlocksKit } from '@/components/editor/basic-blocks-static';// Import your STATIC components for renderingimport { ParagraphElementStatic } from '@/components/editor/paragraph-static';import { HeadingElementStatic } from '@/components/editor/heading-static';// For a styled static output, you might use a wrapper like EditorStaticimport { EditorStatic } from '@/components/editor/editor-static';// Map plugin names to their STATIC rendering componentsconst components = { p: ParagraphElementStatic, // 'p' is the default name for paragraphs h1: HeadingElementStatic, // ... add mappings for all your elements and marks
Styling Serialized HTML
renderStaticHtml returns only the HTML rendered for the editor content itself.
If you use styled components such as EditorStatic or custom static components
with classes, include their CSS wherever the HTML is displayed.
This often means wrapping the serialized HTML in a full HTML document that includes your stylesheets:
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.
components/editor/paragraph-static.tsx
import React from 'react';import type { BaseParagraphPlugin } from 'platejs';import type { PliteElementProps } from 'platejs/static';// Example static paragraph componentexport function ParagraphElementStatic( props: PliteElementProps<typeof BaseParagraphPlugin>) { return ( <PliteElement {...props} className={cn('m-0 px-0 py-1')}> {props.children} </PliteElement> );}
HTML to Plate
The HTML decoder converts strings or DOM elements back into Plate content. It
preserves structure, formatting, and attributes when the installed plugins own
matching codecs.
Basic Usage
Call the root HTML API from a client-side Plate editor.
components/my-html-importer.tsx
import { usePlateEditor } from 'platejs/react';// Import ALL Plate plugins needed to represent the HTML contentimport { BasicBlocksKit } from '@/components/editor/basic-blocks';// ... and so on for bold, italic, tables, lists, etc.function MyHtmlImporter({ htmlString }: { htmlString: string }) { const editor = usePlateEditor({ plugins: [ ...BasicBlocksKit, // Paragraph, headings, blockquote, and horizontal rule // ... include all plugins corresponding to the HTML you expect to parse ], }); const handleImport = () => { const value = editor.api.html.deserialize({ element: htmlString }); if (!value) return; editor.update.value.replace({ children: value }); }; // ... render your editor and a button to trigger handleImport ... return <button onClick={handleImport}>Import HTML</button>;}
components/my-html-importer.tsx
import { usePlateEditor } from 'platejs/react';// Import ALL Plate plugins needed to represent the HTML contentimport { BasicBlocksKit } from '@/components/editor/basic-blocks';// ... and so on for bold, italic, tables, lists, etc.function MyHtmlImporter({ htmlString }: { htmlString: string }) { const editor = usePlateEditor({ plugins: [ ...BasicBlocksKit, // Paragraph, headings, blockquote, and horizontal rule // ... include all plugins corresponding to the HTML you expect to parse ], }); const handleImport = () => { const value = editor.api.html.deserialize({ element: htmlString });
Client-Side Operation
HTML deserialization through editor.api.html.deserialize is
typically a client-side operation because it uses the compiled Plate plugin
model.
Plugin HTML Codec Overview
Each Plate plugin owns the HTML tags, styles, and attributes for its schema
claim. The same 'text/html' map returned by context-bound defineCodecs
handles decode and encode.
Persisted element types come from the installed plugin schema handle (for
example, editor.plugin(ParagraphPlugin).schema.type). The table shows typical associations.
Include the corresponding Plate plugins for these rules to apply.
defineCodecs
Package authors declare node-level HTML meaning in the constructor's codecs
callback before the app's terminal .configure() call. Destructure the
context-bound defineCodecs and pass it the MIME-keyed map. This is the one
inline inference anchor for the plugin schema and codec callbacks:
match is a non-empty array of tag, class, attribute, or style matchers.
decode returns only the value or properties owned by the plugin. Element
codecs do not return type; Plate supplies the installed configured type.
encode returns a wrapper for a mark, a full node spec for an element, or an
attribute/style patch for an element property.
Import-only mappings set decodeOnly: true instead of omitting encode
silently.
priority resolves intentional overlap. Equal-priority exclusive claims
fail model compilation instead of depending on plugin array order.
One plugin may provide a non-empty ordered rule tuple when it owns multiple
HTML representations. Keep that tuple in the same codec map.
This element codec maps <aside> to a callout and preserves an app-configured
storage type:
CalloutPlugin decodes <aside> as a callout element because the codec
targets the installed descriptor. Define a separate named descriptor when a
different persisted identity is required; .configure() does not rename one.
Use defineCodecs(map) for self and product codecs. A plugin contributing HTML
behavior to another descriptor uses defineCodecs(TargetPlugin, map); the
helper injects that target into every rule. Do not put target in the rule
manually. One plugin may keep multiple HTML representations in a non-empty
ordered 'text/html' tuple inside the same map.
Whole-Input HTML Hooks
Use query, transformData, and transformFragment on the plugin's
'text/html' codec for work that needs the complete incoming payload. These
hooks run before or after node decoding; they do not declare node matches.
A React component to wrap the entire editor content during static rendering. Defaults to PlateStatic.
The component receives editor and any props passed here.
Props to pass to the editorComponent. P defaults to PlateStaticProps.
Class name prefixes to preserve when stripClassNames is true. Default preserve list in the stripping helper: ['plite-'].
If true, removes all class names from the output HTML except those whose prefixes are listed in preserveClassNames. Default: false.
If true, removes all data-* attributes from the output HTML. Default: false.
Returns
A promise that resolves to the serialized HTML string.
editor.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.
Parameters
Options for deserialization.
Options
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.
Returns
The deserialized Plate Value, or null when the compiled HTML decoder
rejects an invalid or unsupported result.