Use Plate in Node.js when you need to read, validate, transform, or serialize editor values outside the browser. Node scripts use the base runtime imports, while React editors use /react subpaths. This guide walks through a server-safe editor, Markdown IO, and a content transform.
Do not import from platejs/react or @platejs/*/react in Node.js scripts.
Use createBaseEditor from platejs and base plugins from @platejs/*
packages.
Install the core runtime and the packages your pipeline needs.
| Package | Owns |
|---|---|
platejs | createBaseEditor, core editor APIs, core paragraph behavior. |
@platejs/basic-nodes | Base headings, blockquotes, horizontal rules, and text marks. |
@platejs/markdown | Markdown serialization, deserialization, and the MarkdownPlugin API. |
Create the editor with base plugins only. The editor exposes editor.read(...)
for committed state and editor.update(...) for writes without mounting a
React tree.
import type { Value } from 'platejs';
import { BaseBoldPlugin, BaseHeadingPlugin } from '@platejs/basic-nodes';
import { createBaseEditor } from 'platejs';
const value: Value = [
{
children: [{ text: 'Document Title' }],
type: 'heading', level: 1,
},
{
children: [
{ text: 'With ' },
{ bold: true, text: 'bold' },
{ text: ' text.' },
],
type: 'paragraph',
},
];
const editor = createBaseEditor({
plugins: [BaseHeadingPlugin, BaseBoldPlugin],
initialValue: value,
});
const plainText = editor.read.text.string([]);
console.info(plainText);import type { Value } from 'platejs';
import { BaseBoldPlugin, BaseHeadingPlugin } from '@platejs/basic-nodes';
import { createBaseEditor } from 'platejs';
const value: Value = [
{
children: [{ text: 'Document Title' }],
type: 'heading', level: 1,
},
{
children: [
{ text: 'With ' },
{ bold: true, text: 'bold' },
{ text: ' text.' },
],
type: 'paragraph',
},
Add MarkdownPlugin when the script needs Markdown conversion. Use the
editor's markdown API for both directions.
import { BaseBoldPlugin, BaseHeadingPlugin } from '@platejs/basic-nodes';
import { MarkdownPlugin } from '@platejs/markdown';
import { createBaseEditor } from 'platejs';
const editor = createBaseEditor({
plugins: [BaseHeadingPlugin, BaseBoldPlugin, MarkdownPlugin],
});
const value = editor.api.markdown.deserialize(
[
'# Migration Note',
'',
'Move legacy content into **Plate** format.',
].join('\n')
);
const markdown = editor.api.markdown.serialize({ value });
console.info(markdown);import { BaseBoldPlugin, BaseHeadingPlugin } from '@platejs/basic-nodes';
import { MarkdownPlugin } from '@platejs/markdown';
import { createBaseEditor } from 'platejs';
const editor = createBaseEditor({
plugins: [BaseHeadingPlugin, BaseBoldPlugin, MarkdownPlugin],
});
const value = editor.api.markdown.deserialize(
[
'# Migration Note',
'',
'Move legacy content into **Plate** format.',
].join('\n')
);
const markdown = editor.api.markdown.serialize({ value });
Use transaction groups for migrations and bulk cleanup. Pass at: [] when the
operation should scan the whole document.
import type { Value } from 'platejs';
import {
BaseBoldPlugin,
BaseHeadingPlugin,
} from '@platejs/basic-nodes';
import { MarkdownPlugin } from '@platejs/markdown';
import { createBaseEditor } from 'platejs';
export function normalizeHeadings(value: Value) {
const editor = createBaseEditor({
plugins: [BaseHeadingPlugin, BaseBoldPlugin, MarkdownPlugin],
initialValue: value,
});
const insertAt = editor.read((state) => [state.value.root().length]);
editor.update((tx) => {
tx.nodes.set(
{ type: 'heading', level: 2 },
{
at: [],
match: (node) => 'type' in node && node.type === 'h1',
}
);
tx.nodes.insert(
[{ children: [{ text: 'Imported from the legacy CMS.' }], type: 'paragraph' }],
{ at: insertAt }
);
});
return {
markdown: editor.api.markdown.serialize(),
text: editor.read((state) => state.text.string([])),
value: editor.read((state) => state.value.root()),
};
}import type { Value } from 'platejs';
import {
BaseBoldPlugin,
BaseHeadingPlugin,
} from '@platejs/basic-nodes';
import { MarkdownPlugin } from '@platejs/markdown';
import { createBaseEditor } from 'platejs';
export function normalizeHeadings(value: Value) {
const editor = createBaseEditor({
plugins: [BaseHeadingPlugin, BaseBoldPlugin, MarkdownPlugin],
initialValue: value,
});
const insertAt = editor.read((state) => [state.value.
| Runtime | Import from | Use for |
|---|---|---|
| Node.js scripts | platejs, @platejs/* | Migration, validation, serialization, search indexing. |
| React editors | platejs/react, @platejs/*/react | Editable UI, hooks, rendered components, toolbar behavior. |
| Static rendering | platejs/static | Server-rendered read-only content. |
Plugin packages can expose both base and React entrypoints. In Node.js, choose the base entrypoint even when the same feature has React components for the browser editor.
| API | Package | Notes |
|---|---|---|
createBaseEditor | platejs | Creates a non-React editor instance. |
editor.read((state) => state.text.string([])) | platejs | Reads text from the whole document. |
editor.update((tx) => tx.nodes.set(...)) | platejs | Updates matching nodes. Use at: [] for document-wide transforms. |
editor.update((tx) => tx.nodes.insert(...)) | platejs | Inserts nodes at a path. |
editor.api.markdown.deserialize | @platejs/markdown | Converts Markdown into a Plate value. |
editor.api.markdown.serialize | @platejs/markdown | Converts the editor value or an explicit value option to Markdown. |
| Task | Guide |
|---|---|
| Serialize to Markdown | Markdown |
| Serialize to HTML | HTML |
| Render read-only content | Static Rendering |
| Query editor state | Editor API |
| Apply transforms | Editor Transforms |
Done. You now have a server-safe Plate runtime that can power migration scripts, validation jobs, and content serialization.