Skip to main content
Yoopta Editor uses a structured JSON format for content storage and provides powerful serialization APIs to convert between formats.

Content Structure

Yoopta content is stored as a flat object of blocks:
type YooptaContentValue = Record<string, YooptaBlockData>;

type YooptaBlockData = {
  id: string;
  type: string;           // PascalCase: "Paragraph", "HeadingOne"
  value: SlateElement[];  // Slate elements with kebab-case types
  meta: YooptaBlockBaseMeta;
};

type YooptaBlockBaseMeta = {
  order: number;   // Determines display order
  depth: number;   // Nesting level (0 = root)
  align?: 'left' | 'center' | 'right';
};

Example Content

{
  "block-1": {
    "id": "block-1",
    "type": "HeadingOne",
    "value": [
      {
        "id": "elem-1",
        "type": "heading-one",
        "children": [{ "text": "My Title" }],
        "props": { "nodeType": "block" }
      }
    ],
    "meta": { "order": 0, "depth": 0 }
  },
  "block-2": {
    "id": "block-2",
    "type": "Paragraph",
    "value": [
      {
        "id": "elem-2",
        "type": "paragraph",
        "children": [
          { "text": "This is " },
          { "text": "bold", "bold": true },
          { "text": " text." }
        ],
        "props": { "nodeType": "block" }
      }
    ],
    "meta": { "order": 1, "depth": 0 }
  }
}

Slate Elements

Each block contains Slate elements (descendants):
type SlateElement = {
  id: string;
  type: string;           // kebab-case: "paragraph", "heading-one"
  children: Descendant[];
  props?: {
    nodeType: 'block' | 'inline' | 'void' | 'inlineVoid';
    [key: string]: any;   // Plugin-specific props
  };
};

Text Nodes with Marks

type TextNode = {
  text: string;
  // Text marks (optional)
  bold?: boolean;
  italic?: boolean;
  underline?: boolean;
  strike?: boolean;
  code?: boolean;
  highlight?: { color: string; backgroundColor: string };
};

Serialization Helpers

Yoopta provides helper functions to serialize text nodes:

serializeTextNodes

Convert text nodes to HTML:
import { serializeTextNodes } from '@yoopta/editor';

const children = [
  { text: 'Normal ' },
  { text: 'bold', bold: true },
  { text: ' and ' },
  { text: 'italic', italic: true },
];

const html = serializeTextNodes(children);
// Output: "Normal <strong>bold</strong> and <em>italic</em>"

serializeTextNodesIntoMarkdown

Convert text nodes to Markdown:
import { serializeTextNodesIntoMarkdown } from '@yoopta/editor';

const children = [
  { text: 'Normal ' },
  { text: 'bold', bold: true },
  { text: ' and ' },
  { text: 'italic', italic: true },
];

const markdown = serializeTextNodesIntoMarkdown(children);
// Output: "Normal **bold** and _italic_"

Plugin Parsers

Plugins define how their content is serialized:
import { YooptaPlugin, serializeTextNodes, serializeTextNodesIntoMarkdown } from '@yoopta/editor';

const Paragraph = new YooptaPlugin({
  type: 'Paragraph',
  elements: {
    paragraph: {
      render: (props) => <p {...props.attributes}>{props.children}</p>,
    },
  },
  parsers: {
    html: {
      deserialize: {
        nodeNames: ['P'],
      },
      serialize: (element, text, blockMeta) => {
        const { align = 'left', depth = 0 } = blockMeta || {};
        return `<p style="text-align: ${align}; margin-left: ${depth * 20}px">
          ${serializeTextNodes(element.children)}
        </p>`;
      },
    },
    markdown: {
      serialize: (element) => {
        return `${serializeTextNodesIntoMarkdown(element.children)}\n`;
      },
    },
    email: {
      serialize: (element, text, blockMeta) => {
        const { align = 'left', depth = 0 } = blockMeta || {};
        return `<table style="width: 100%">
          <tbody>
            <tr>
              <td>
                <p style="text-align: ${align}; margin-left: ${depth * 20}px">
                  ${serializeTextNodes(element.children)}
                </p>
              </td>
            </tr>
          </tbody>
        </table>`;
      },
    },
  },
});

HTML Deserialization

Convert HTML to Yoopta content:
import { deserializeHTML } from '@yoopta/editor';

const htmlString = '<h1>Title</h1><p>Paragraph</p>';
const parsedHtml = new DOMParser().parseFromString(htmlString, 'text/html');

// Returns array of YooptaBlockData
const blocks = deserializeHTML(editor, parsedHtml.body);

// Convert to YooptaContentValue
const value: YooptaContentValue = {};
blocks.forEach((block, i) => {
  block.meta.order = i;
  value[block.id] = block;
});

editor.setEditorValue(value);

Custom HTML Deserializer

Plugins can provide custom HTML parsing:
parsers: {
  html: {
    deserialize: {
      nodeNames: ['DIV'],
      parse: (el: HTMLElement, editor: YooEditor) => {
        if (el.className === 'custom-block') {
          // Return a YooptaBlockData
          return {
            id: generateId(),
            type: 'CustomBlock',
            value: [
              {
                id: generateId(),
                type: 'custom-block',
                children: [{ text: el.textContent || '' }],
                props: { nodeType: 'block' },
              },
            ],
            meta: { order: 0, depth: 0 },
          };
        }
      },
    },
  },
}

Text Node Deserialization

Deserialize HTML text nodes with formatting:
import { deserializeTextNodes } from '@yoopta/editor';

const html = '<strong>Bold</strong> and <em>italic</em> text';
const div = document.createElement('div');
div.innerHTML = html;

const textNodes = deserializeTextNodes(div.childNodes);
// Result:
// [
//   { text: 'Bold', bold: true },
//   { text: ' and ' },
//   { text: 'italic', italic: true },
//   { text: ' text' },
// ]

Editor Parser Methods

The editor provides built-in parser methods:
// Get HTML
const html = editor.getHTML(content);

// Get Markdown
const markdown = editor.getMarkdown(content);

// Get Plain Text
const text = editor.getPlainText(content);

// Get Email HTML
const email = editor.getEmail(content, options);

// Get Yoopta JSON (passthrough)
const json = editor.getYooptaJSON(content);

Building Content Programmatically

Use the editor.y helper to create elements:
// Create a block element
const paragraph = editor.y('paragraph', {
  props: { nodeType: 'block' },
  children: [
    editor.y.text('Hello '),
    editor.y.text('world', { bold: true }),
  ],
});

// Create an inline element
const link = editor.y.inline('link', {
  props: { url: 'https://yoopta.dev', nodeType: 'inline' },
  children: [editor.y.text('Click here')],
});

// Create a text node
const text = editor.y.text('Formatted text', {
  bold: true,
  italic: true,
});

Building Complete Blocks

import { buildBlockData } from '@yoopta/editor';

const blockData = buildBlockData(editor, {
  type: 'Paragraph',
  value: [
    editor.y('paragraph', {
      children: [
        editor.y.text('This is a paragraph'),
      ],
    }),
  ],
  meta: { order: 0, depth: 0 },
});

editor.insertBlock('Paragraph', {
  blockData,
  focus: true,
});

Content Validation

Validate content structure:
import { validateYooptaValue } from '@yoopta/editor';

const isValid = validateYooptaValue(content);

if (!isValid) {
  console.error('Invalid content structure');
}

Performance Tips

Cache serialization results for large documents:
const cache = new Map();

function getCachedHTML(content) {
  const key = JSON.stringify(content);
  if (cache.has(key)) return cache.get(key);
  
  const html = editor.getHTML(content);
  cache.set(key, html);
  return html;
}
For real-time previews, debounce serialization:
const debouncedSerialize = debounce(() => {
  const html = editor.getHTML(editor.getEditorValue());
  setPreview(html);
}, 300);

editor.on('change', debouncedSerialize);
When importing large documents, use batch operations:
editor.batchOperations(() => {
  const blocks = deserializeHTML(editor, htmlString);
  blocks.forEach(block => {
    editor.insertBlock(block.type, { blockData: block });
  });
});

Custom Formats

Create custom serialization formats:
function serializeToCustomFormat(editor: YooEditor, content: YooptaContentValue) {
  const blocks = Object.values(content).sort((a, b) => a.meta.order - b.meta.order);
  
  return blocks.map(block => {
    const plugin = editor.plugins[block.type];
    const element = block.value[0];
    
    // Custom serialization logic
    return {
      type: block.type,
      text: editor.getPlainText({ [block.id]: block }),
      meta: block.meta,
    };
  });
}

Versioning

Add version metadata for content migrations:
type VersionedContent = {
  version: string;
  content: YooptaContentValue;
  metadata: {
    created: string;
    updated: string;
    author: string;
  };
};

function saveContent(editor: YooEditor) {
  const versioned: VersionedContent = {
    version: '1.0.0',
    content: editor.getEditorValue(),
    metadata: {
      created: new Date().toISOString(),
      updated: new Date().toISOString(),
      author: 'user-id',
    },
  };
  
  return JSON.stringify(versioned);
}

Build docs developers (and LLMs) love