<PlateStatic> is a fast, read-only React component for rendering Plate content, optimized for server-side or React Server Component (RSC) environments. It avoids client-side editing logic and memoizes node renders for better performance compared to using <Plate> in read-only mode.
It is the rendering path behind renderStaticHtml and fits server or RSC
surfaces that need a non-interactive Plate view.
<PlateStatic>For interactive read-only features (like comment popovers or selections), use the standard <Plate> component in the browser. For purely server-rendered, non-interactive content, <PlateStatic> is the recommended choice.
The fastest way to enable static rendering is with the BaseEditorKit, which includes pre-configured base plugins that work seamlessly with server-side rendering.
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';
import { createBaseEditor } from 'platejs';
import { PlateStatic } from 'platejs/static';
import { BaseEditorKit } from '@/components/editor/plugins-static';
const editor = createBaseEditor({
plugins: BaseEditorKit,
initialValue: [
{ type: 'heading', level: 1, children: [{ text: 'Server-Rendered Title' }] },
{ type: 'paragraph', children: [{ text: 'This content is rendered statically.' }] },
],
});
// Render statically
export default function MyStaticPage() {
return <PlateStatic editor={editor} />;
}See a complete server-side static rendering 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,
Initialize a Plite editor instance using createBaseEditor with your required plugins and components. This is analogous to using usePlateEditor for the interactive <Plate> component.
import { createBaseEditor } from 'platejs';
import { BaseBasicBlocksKit } from '@/components/editor/basic-blocks-static';
// Import any other desired base plugins, such as MarkdownPlugin.
// Ensure you are NOT importing from /react subpaths for server environments.
const editor = createBaseEditor({
plugins: [
...BaseBasicBlocksKit,
// Add other base plugins here.
],
initialValue: [
{
type: 'paragraph',
children: [{ text: 'Hello from a static Plate editor!' }],
},
],
});import { createBaseEditor } from 'platejs';
import { BaseBasicBlocksKit } from '@/components/editor/basic-blocks-static';
// Import any other desired base plugins, such as MarkdownPlugin.
// Ensure you are NOT importing from /react subpaths for server environments.
const editor = createBaseEditor({
plugins: [
...BaseBasicBlocksKit,
// Add other base plugins here.
],
initialValue: [
{
type: 'paragraph',
children: [{ text: 'Hello from a static Plate editor!' }],
},
],
});If your interactive editor uses client-side components (e.g., with use client or event handlers), you must create static, server-safe equivalents. These components should render pure HTML without browser-specific logic.
import React from 'react';
import type { BaseParagraphPlugin } from 'platejs';
import type { PliteElementProps } from 'platejs/static';
export function ParagraphElementStatic(
props: PliteElementProps<typeof BaseParagraphPlugin>
) {
return (
<PliteElement {...props}>
{props.children}
</PliteElement>
);
}Create similar static components for headings, images, links, etc.
Create an object that maps persisted element types or property keys to their corresponding static React components, then pass it to the editor.
import { ParagraphElementStatic } from './ui/paragraph-static';
import { HeadingElementStatic } from './ui/heading-static';
// ... import other static components
export const staticComponents = {
p: ParagraphElementStatic,
h1: HeadingElementStatic,
// ... add mappings for all your element and leaf types
};import { ParagraphElementStatic } from './ui/paragraph-static';
import { HeadingElementStatic }
<PlateStatic>Use the <PlateStatic> component, providing the editor instance configured with your components.
import { createBaseEditor } from 'platejs';
import { PlateStatic } from 'platejs/static';
import { BaseBasicBlocksKit } from '@/components/editor/basic-blocks-static';
import { staticComponents } from '@/components/static-components';
export default async function MyStaticPage() {
// Example: Fetch or define editor value
const initialValue = [
{ type: 'heading', level: 1, children: [{ text: 'Server-Rendered Title' }] },
{ type: 'paragraph', children: [{ text: 'Content rendered statically.' }] },
];
const editor = createBaseEditor
<PlateStatic> enhances performance through memoization:
<ElementStatic> and <LeafStatic> is wrapped in React.memo.For cases where you need minimal interactivity with static content, use <PlateView>. This component wraps <PlateStatic> and adds client-side event handlers for user interactions while maintaining the performance benefits of static rendering.
import { createStaticEditor, PlateStatic } from 'platejs/static';
import { BaseEditorKit } from '@/components/editor/plugins-static';
import { InteractiveViewer } from './interactive-viewer';
export default async function DocumentPage() {
const content = await fetchDocument(); // Your document data
// Server-side static editor
const editor = createStaticEditor({
plugins: BaseEditorKit,
initialValue: content,
});
return (
<div className="grid grid-cols-2 gap-4">
{/* Pure static rendering - no interactivity */}
<div>
<h2>Static View (Server Rendered)</h2>
<PlateStatic editor={editor} />
</div>
{/* Interactive view - rendered on client */}
<div>
<h2>Interactive View</h2>
<InteractiveViewer value={content} />
</div>
</div>
);
}import { createStaticEditor, PlateStatic } from 'platejs/static';
import { BaseEditorKit } from '@/components/editor/plugins-static';
import { InteractiveViewer } from './interactive-viewer';
export default async function DocumentPage() {
const content = await fetchDocument(); // Your document data
// Server-side static editor
const editor = createStaticEditor({
plugins: BaseEditorKit,
initialValue: content,
});
return (
<div className="grid grid-cols-2 gap-4">
{/* Pure static rendering - no interactivity */}
<div
'use client';
import { usePlateViewEditor } from 'platejs/react';
import { PlateView } from 'platejs/react';
import { BaseEditorKit } from '@/components/editor/plugins-static';
export function InteractiveViewer({ value }) {
const editor = usePlateViewEditor({
plugins: BaseEditorKit,
initialValue: value,
});
return <PlateView editor={editor} />;
}'use client';
import { usePlateViewEditor } from 'platejs/react';
import { PlateView } from 'platejs/react';
import { BaseEditorKit } from '@/components/editor/plugins-static';
export function InteractiveViewer({ value }) {
const editor = usePlateViewEditor({
plugins: BaseEditorKit,
initialValue: value,
});
return <PlateView editor={editor} />;
}'use client' directivePlateStatic internally for renderingusePlateViewEditor: Creates a static editor optimized for view-only React componentsViewPlugin which provides event handling capabilitiesPlateView cannot be used in Server Components. If you're passing an editor from a server component to a client component, you'll encounter serialization errors. Use PlateStatic on the server side, or create the editor client-side with usePlateViewEditor.
| Aspect | <PlateStatic> | <PlateView> | <Plate> + readOnly |
|---|---|---|---|
| Environment | Server/Client (SSR/RSC safe) | Client-only | Client-only |
| Interactivity | None | Minimal (selection, copy, toolbar, etc.) | Full interactive features (browser-only) |
| Browser APIs | Not used | Minimal (event handlers) | Full usage |
| Performance | Best - static HTML only | Good - static rendering + event delegation | Heavier - full editor internals |
| Bundle Size | Smallest | Small | Largest |
| Use Cases | Server rendering, HTML export | Client-side content with basic interactions | Full read-only editor with all features |
| Recommendation | SSR/RSC without any interactions | Client-side content needing light interactivity | Client-side with complex interactive needs |
In a Next.js App Router (or similar RSC environment), <PlateStatic> can be used directly in Server Components:
import { createBaseEditor } from 'platejs';
import { PlateStatic } from 'platejs/static';
import { BaseBasicBlocksKit } from '@/components/editor/basic-blocks-static';
import { staticComponents } from '@/components/static-components'; // Your static components mapping
export default async function Page() {
// Fetch or define content server-side
const serverContent = [
{ type: 'heading', level: 1, children: [{ text: 'Rendered on the Server! 🎉' }] },
{ type: 'paragraph', children: [{ text: 'This content is static and server-rendered.' }] },
];
const editor = createBaseEditor({
plugins: [...BaseBasicBlocksKit],
components: staticComponents,
initialValue: serverContent,
});
return (
<PlateStatic
editor={editor}
className="my-static-preview-container"
/>
);
}import { createBaseEditor } from 'platejs';
import { PlateStatic } from 'platejs/static';
import { BaseBasicBlocksKit } from '@/components/editor/basic-blocks-static';
import { staticComponents } from '@/components/static-components'; // Your static components mapping
export default async function Page() {
// Fetch or define content server-side
const serverContent = [
{ type: 'heading', level: 1, children: [{ text: 'Rendered on the Server! 🎉' }] },
{ type: 'paragraph', children: [{ text: 'This content is static and server-rendered.' }] },
];
const editor = createBaseEditor({
plugins: [
This renders the content to HTML on the server without needing a client-side JavaScript bundle for PlateStatic itself.
For server-rendering the static Plate tree to an HTML string, use
renderStaticHtml. It renders <PlateStatic> through React DOM Server; it is
not a semantic HTML codec.
import { createBaseEditor } from 'platejs';
import { renderStaticHtml } from 'platejs/static';
import { BaseBasicBlocksKit } from '@/components/editor/basic-blocks-static';
import { staticComponents } from '@/components/static-components';
async function getDocumentAsHtml(value: any[]) {
const editor = createBaseEditor({
plugins: [...BaseBasicBlocksKit],
components: staticComponents,
initialValue: value,
});
const html = await renderStaticHtml(editor, {
// editorComponent: PlateStatic, // Optional: Defaults to PlateStatic
props: { className: 'prose max-w-none' }, // Example: Pass props to the root div
});
return html;
}
// Example Usage:
// const value = [ { type: 'heading', level: 1, children: [{ text: 'My Document' }] } ];
// getDocumentAsHtml(value).then(console.log);import { createBaseEditor } from 'platejs';
import { renderStaticHtml } from 'platejs/static';
import { BaseBasicBlocksKit } from '@/components/editor/basic-blocks-static';
import { staticComponents } from '@/components/static-components';
async function getDocumentAsHtml(value: any[]) {
const editor = createBaseEditor({
plugins: [...BaseBasicBlocksKit],
components: staticComponents,
initialValue: value,
});
const html = await renderStaticHtml(editor, {
// editorComponent: PlateStatic, // Optional: Defaults to PlateStatic
props: { className: 'prose max-w-none' }, // Example: Pass props to the root div
For more details, see the HTML Serialization guide.
import type React from 'react';
import type { BaseEditor } from 'platejs';
interface PlateStaticProps<E = BaseEditor>
extends React.HTMLAttributes<HTMLDivElement> {
/**
* The Plate editor instance, created via `createBaseEditor`.
* Must include plugins and components relevant to the content being rendered.
*/
editor: E;
/** Inline CSS styles for the root `div` element. */
style?: React.CSSProperties;
// Other HTMLDivElement attributes like `className`, `id`, etc., are also supported.
}import type React from 'react';
import type { BaseEditor } from 'platejs';
interface PlateStaticProps<E = BaseEditor>
extends React.HTMLAttributes<HTMLDivElement> {
/**
* The Plate editor instance, created via `createBaseEditor`.
* Must include plugins and components relevant to the content being rendered.
*/
editor: E;
/** Inline CSS styles for the root `div` element. */
style?: React.CSSProperties;
// Other HTMLDivElement attributes like `className`, `id`, etc., are also supported.
}editor: An editor created with createBaseEditor or createStaticEditor, including the plugins and components required by the value.import { createBaseEditor } from 'platejs';
import { PlateStatic } from 'platejs/static';
import { BaseEditorKit } from '@/components/editor/plugins-static';
const editor = createBaseEditor({
plugins: BaseEditorKit,
initialValue: [
{ type: 'heading', level: 1, children: [{ text: 'Server-Rendered Title' }] },
{ type: 'paragraph', children: [{ text: 'This content is rendered statically.' }] },
],
});
// Render statically
export default function MyStaticPage() {
return <PlateStatic editor={editor} />;
}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,
} from '@/components/editor/plate-to-html';
import { PlateToHtmlEditorKit } from '@/components/editor/plate-to-html-kit';
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';
export const metadata: Metadata = {
title: 'Plate to HTML',
};
const getCachedTailwindCss = React.cache(async () => {
const cssPath = path.join(process.cwd(), 'public', 'tailwind.css');
return await fs.readFile(cssPath, 'utf-8');
});
const createHtmlDocument = ({
editorHtml,
katexCDN,
tailwindCss,
theme,
}: {
editorHtml: string;
tailwindCss: string;
katexCDN?: string;
theme?: string;
}) => `<!DOCTYPE html>
<html lang="en"${theme === 'dark' ? ' class="dark"' : ''}>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="color-scheme" content="light dark" />
<style>${tailwindCss}</style>
${katexCDN}
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400..700&family=JetBrains+Mono:wght@400..700&display=swap"
rel="stylesheet"
/>
<style>
:root {
--font-sans: 'Inter', 'Inter Fallback';
--font-mono: 'JetBrains Mono', 'JetBrains Mono Fallback';
}
</style>
</head>
<body>
${editorHtml}
</body>
</html>`;
const createValue = (): EditorDocumentValue => ({
children: [
...basicBlocksValue,
...basicMarksValue,
...tocPlaygroundValue,
...linkValue,
...tableValue,
...equationValue,
...columnValue,
...mentionValue,
...dateValue,
...fontValue,
...discussionValue,
...alignValue,
...lineHeightValue,
...indentValue,
...listValue,
...mediaValue.children,
],
});
export default async function PlateToHtmlBlock() {
const editor = createStaticEditor({
plugins: PlateToHtmlEditorKit,
initialValue: createValue(),
});
const tailwindCss = await getCachedTailwindCss();
const katexCDN = `<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.18/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 renderStaticHtml(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()}>EditorView</h3>
<EditorViewClient value={createValue()} />
</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 font-semibold text-xl tracking-tight'
);import React from 'react';
import type { BaseParagraphPlugin } from 'platejs';
import type { PliteElementProps } from 'platejs/static';
export function ParagraphElementStatic(
props: PliteElementProps<typeof BaseParagraphPlugin>
) {
return (
<PliteElement {...props}>
{props.children}
</PliteElement>
);
}import { createBaseEditor } from 'platejs';
import { PlateStatic } from 'platejs/static';
import { BaseBasicBlocksKit } from '@/components/editor/basic-blocks-static';
import { staticComponents } from '@/components/static-components';
export default async function MyStaticPage() {
// Example: Fetch or define editor value
const initialValue = [
{ type: 'heading', level: 1, children: [{ text: 'Server-Rendered Title' }] },
{ type: 'paragraph', children: [{ text: 'Content rendered statically.' }] },
];
const editor = createBaseEditor({
plugins: [...BaseBasicBlocksKit],
components: staticComponents,
initialValue,
});
return (
<PlateStatic
editor={editor}
style={{ padding: 16 }}
className="my-plate-static-content"
/>
);
}