Serializing HTML

HTML <-> Slate

Loading...
Files
components/demo.tsx
'use client';

import React from 'react';

import { Plate } from '@udecode/plate-common/react';

import { editorPlugins } from '@/components/editor/plugins/editor-plugins';
import { useCreateEditor } from '@/components/editor/use-create-editor';
import { Editor, EditorContainer } from '@/components/plate-ui/editor';

import { DEMO_VALUES } from './values/demo-values';

export default function Demo({ id }: { id: string }) {
  const editor = useCreateEditor({
    plugins: [...editorPlugins],
    value: DEMO_VALUES[id],
  });

  return (
    <Plate editor={editor}>
      <EditorContainer variant="demo">
        <Editor />
      </EditorContainer>
    </Plate>
  );
}

Slate -> HTML

Server-side example

Usage

// ...
import { createSlateEditor, serializeHtml } from '@udecode/plate-common';
import { EditorStatic } from '@/components/plate-ui/editor-static';
 
// Create an editor and configure all the plugins you need
const editor = createSlateEditor({
  // ... your plugins ...
});
 
// Provide the components that map Slate nodes to HTML elements
const components = {
  // [ParagraphPlugin.key]: ParagraphElementStatic,
  // [HeadingPlugin.key]: HeadingElementStatic,
  // ...
};
 
// You can also pass a custom editor component and props
// For example, EditorStatic is a styled version of PlateStatic.
const html = await serializeHtml(editor, {
  components,
  editorComponent: EditorStatic, // defaults to PlateStatic if not provided
  props: { variant: 'none', className: 'p-2' },
});

If you use a custom component, like EditorStatic, you must also ensure that all required styles and classes are included in your final HTML file. Since serialize only returns the inner editor HTML, you may need to wrap it in a full HTML document with any external CSS, scripts, or <style> tags.

For example:

// After serializing the content:
const html = await serializeHtml(editor, {
  components,
  editorComponent: EditorStatic,
  props: { variant: 'none' },
});
 
// Wrap the HTML in a full HTML document
const fullHtml = `<!DOCTYPE html>
<html>
  <head>
    <link rel="stylesheet" href="/path/to/tailwind.css" />
    <!-- other head elements -->
  </head>
  <body>
    ${html}
  </body>
</html>`;

Using Static Components

When serializing Slate content to HTML, you must use static versions of your components. Your interactive, "client" components often rely on browser APIs or client-side React features that are not available during server-side rendering (SSR).

What does this mean?

  • Interactive Components: Components that use use client, event handlers (onClick), or browser-only APIs (like window or document) cannot run on the server.
  • Static Components: Instead, for serialization, you must provide "static" versions of these components. Static components should:
    • Not include any event handlers like onClick.
    • Not rely on browser APIs or client-side hooks.
    • Simply render the content as plain HTML.

Example:

If you have a client-only component like ImageElement that uses use client and event handlers, you need to create a static version ImageElementStatic that just returns a plain <img> tag with all its attributes and no interactivity.

For each plugin key, provide its corresponding static component in the components object passed to serializeHtml:

// Instead of using interactive components that rely on 'use client',
// use statically defined components that simply return plain HTML.
 
import { createSlateEditor, serializeHtml } from '@udecode/plate-common';
 
// Import static versions of your components
import { ParagraphElementStatic } from '@/components/plate-ui/paragraph-element-static';
import { HeadingElementStatic } from '@/components/plate-ui/heading-element-static';
import { ImageElementStatic } from '@/components/plate-ui/image-element-static';
// ... and so on for each plugin ...
 
const editor = createSlateEditor({ /* ...plugins... */ });
 
const components = {
  [BaseParagraphPlugin.key]: ParagraphElementStatic,
  [BaseHeadingPlugin.key]: HeadingElementStatic,
  [BaseImagePlugin.key]: ImageElementStatic,
  // ... other static components ...
};
 
const html = await serializeHtml(editor, { components });

PlateStatic vs. Plate

When rendering your editor output without the need for an interactive environment, you have two primary options: PlateStatic and Plate. Both are designed to produce static, non-editable views of your content, but they differ in purpose and performance characteristics.

PlateStatic

  • Purpose: A more lightweight, server-compatible static renderer of the editor content.
  • Performance: Generally more performant than Plate in read-only mode.
  • Use Case: Ideal when you need a static preview of the content (e.g., in a server-side render or a static build). PlateStatic should be the default choice if you want a fast, static representation of your content.

Plate (Read-Only)

  • Purpose: The standard Plate editor component used with readOnly={true}. While this prevents editing, it still operates as a client component relying on browser APIs.
  • Use Case: Suitable if you’re rendering in a browser and want an interactive environment (e.g. comments) that just happens to be non-editable. It’s not suitable for server-side HTML serialization or scenarios where static output is required, as it still runs client-side code.

In summary:

  • If you need high performance, a static view or HTML serialization: Use PlateStatic.
  • If you need an interactive environment and can run in a browser, but just want it read-only: Use Plate with readOnly={true}.

API

serializeHtml

Convert Slate Nodes into HTML string.

Parameters

Collapse all

    Options to control the HTML serialization process.

Returns

Collapse all

    A HTML string representing the Slate content.

HTML -> Slate

Usage

The editor.api.html.deserialize function allows you to convert HTML content into Slate value:

import { createPlateEditor } from '@udecode/plate-common/react';
 
const editor = createPlateEditor({
  plugins: [
    // all plugins that you want to deserialize
  ]
})
editor.children = editor.api.html.deserialize('<p>Hello, world!</p>')

Plugin Deserialization Rules

Here's a comprehensive list of plugins that support HTML deserialization, along with their corresponding HTML elements and styles:

Text Formatting

  • BoldPlugin: <strong>, <b>, or style="font-weight: 600|700|bold"
  • ItalicPlugin: <em>, <i>, or style="font-style: italic"
  • UnderlinePlugin: <u> or style="text-decoration: underline"
  • StrikethroughPlugin: <s>, <del>, <strike>, or style="text-decoration: line-through"
  • SubscriptPlugin: <sub> or style="vertical-align: sub"
  • SuperscriptPlugin: <sup> or style="vertical-align: super"
  • CodePlugin: <code> or style="font-family: Consolas" (not within a <pre> tag)
  • KbdPlugin: <kbd>

Paragraphs and Headings

  • ParagraphPlugin: <p>
  • HeadingPlugin: <h1>, <h2>, <h3>, <h4>, <h5>, <h6>

Lists

  • ListPlugin:
    • Unordered List: <ul>
    • Ordered List: <ol>
    • List Item: <li>
  • IndentListPlugin:
    • List Item: <li>
    • Parses aria-level attribute for indentation

Blocks

  • BlockquotePlugin: <blockquote>
  • CodeBlockPlugin:
    • Deserializes <pre> elements
    • Deserializes <p> elements with fontFamily: 'Consolas' style
    • Splits content into code lines
    • Preserves language information if available
  • HorizontalRulePlugin: <hr>
  • LinkPlugin: <a>
  • ImagePlugin: <img>
  • MediaEmbedPlugin: <iframe>

Tables

  • TablePlugin:
    • Table: <table>
    • Table Row: <tr>
    • Table Cell: <td>
    • Table Header Cell: <th>

Text Styling

  • FontBackgroundColorPlugin: style="background-color: *"
  • FontColorPlugin: style="color: *"
  • FontFamilyPlugin: style="font-family: *"
  • FontSizePlugin: style="font-size: *"
  • FontWeightPlugin: style="font-weight: *"
  • HighlightPlugin: <mark>

Layout and Formatting

  • AlignPlugin: style="text-align: *"
  • LineHeightPlugin: style="line-height: *"

Deserialization Properties

Plugins can define various properties to control HTML deserialization:

  • parse: A function to parse an HTML element into a Plate node
  • query: A function that determines if the deserializer should be applied
  • rules: An array of rule objects that define valid HTML elements and attributes
  • isElement: Indicates if the plugin deserializes elements
  • isLeaf: Indicates if the plugin deserializes leaf nodes
  • attributeNames: List of HTML attribute names to store in node.attributes
  • withoutChildren: Excludes child nodes from deserialization
  • rules: Array of rule objects for element matching
    • validAttribute: Valid element attributes
    • validClassName: Valid CSS class name
    • validNodeName: Valid HTML tag names
    • validStyle: Valid CSS styles

Extending Deserialization

You can extend or customize the deserialization behavior of any plugin. Here's an example of how you might extend the CodeBlockPlugin:

import { CodeBlockPlugin } from '@udecode/plate-code-block/react';
 
const CustomCodeBlockPlugin = CodeBlockPlugin.extend({
  parsers: {
    html: {
      deserializer: {
        parse: ({ element }) => {
          const language = element.getAttribute('data-language');
          const textContent = element.textContent || '';
          const lines = textContent.split('\n');
          
          return {
            type: CodeBlockPlugin.key,
            language,
            children: lines.map((line) => ({
              type: CodeLinePlugin.key,
              children: [{ text: line }],
            })),
          };
        },
        rules: [
          ...CodeBlockPlugin.parsers.html.deserializer.rules,
          { validAttribute: 'data-language' },
        ],
      },
    },
  },
});

This customization adds support for a data-language attribute in code block deserialization and preserves the language information.

Advanced Deserialization Example

The IndentListPlugin provides a more complex deserialization process:

  1. It transforms HTML list structures into indented paragraphs.
  2. It handles nested lists by preserving the indentation level.
  3. It uses the aria-level attribute to determine the indentation level.

Here's a simplified version of its deserialization logic:

export const IndentListPlugin = createTSlatePlugin<IndentListConfig>({
  // ... other configurations ...
  parsers: {
    html: {
      deserializer: {
        isElement: true,
        parse: ({ editor, element, getOptions }) => ({
          indent: Number(element.getAttribute('aria-level')),
          listStyleType: element.style.listStyleType,
          type: editor.getType(ParagraphPlugin),
        }),
        rules: [
          {
            validNodeName: 'LI',
          },
        ],
      },
    },
  },
});

API

editor.api.html.deserialize

Deserialize HTML string into Slate value.

Parameters

    options.element HTMLElement | string

    The HTML element or string to deserialize.

    options.collapseWhiteSpace optional boolean

    Flag to enable or disable the removal of whitespace from the serialized HTML.

    • Default: true (Whitespace will be removed.)

Returns

Collapse all

    The deserialized Slate value.