---
title: "View-рендерер Blok — HTML из JSON без редактора"
description: "Превращайте OutputData в безопасный HTML или простой текст через blocksToHtml — без DOM и экземпляра редактора, плюс BlokView и useBlokView для React."
source: https://blokeditor.com/ru/docs/view-api/
lastmod: 2026-09-07
---

Фреймворк JavaScript

Основное View-рендерер

На этой странице blocksToHtml(data, options?)

# View-рендерер: показ документов без редактора

Показывайте сохранённые документы, не платя за редактор. Подпуть @bloklabs/core/view синхронно и без DOM превращает OutputData в семантический HTML или простой текст — он работает в Node, воркерах и React Server Components, — поэтому страницам «только для чтения» (публикации, превью, поисковая индексация, письма) больше не нужны экземпляр редактора, его бандл и асинхронное ожидание готовности. Каждое строчное поле санитизируется по составленному allowlist до подстановки, а политика URL-схем идентична редакторской; используйте функции вместе с defineBlokSchema — и документы отображаются под тем же составом санитизации, который их создал (если набор строчных инструментов меняется на лету через tools.setInlineToolbar, пересоберите схему, чтобы отображение не отстало). Для React очевидный путь «только для чтения» — <BlokView> (и useBlokView без обёртки): берите их вместо <BlokEditor readOnly>, который отгружает каждому читателю полный редакторский рантайм (панель инструментов, историю, механику мутаций). Вывод по умолчанию без стилей: подключите classes + root вместе с опциональным @bloklabs/core/view.css для паритета с редактором либо только toolAttributes с той же таблицей стилей — это бесклассовая база, воспроизводящая межблочные отступы редактора из тех же токенов --blok-block-padding-*; включите blockIds для глубоких ссылок «скопировать ссылку на блок» и передайте transformUrl, чтобы переписывать href и URL изображений в CDN.

Обновлено 7 авг. 2026 г. [Редактировать на GitHub](https://github.com/JackUait/blok/blob/main/docs/src/components/api/api-data.ts)

### Как получить экземпляр редактора

Методы ниже вызываются на редакторе, созданном через new Blok(). Они доступны после того, как разрешится editor.isReady.

TypeScript

```
// You already hold the instance returned by the constructor.
const editor = new Blok({ holder: 'editor' });
await editor.isReady;

// Call any API method on it.
editor.caret.setToLastBlock('end');
```

## Методы

### blocksToHtml(data, options?)

string

Отрисовать сохранённый документ в семантический HTML — синхронно и без DOM, поэтому безопасно в Node, воркерах и React Server Components. Каждое строчное поле санитизируется по составленному allowlist до подстановки, а политика URL-схем идентична редакторской. Возвращает '' для пустых или некорректных документов (nullish на входе допускается).

Параметры

| Параметр | Тип | Обязательный | По умолчанию | Описание |
| --- | --- | --- | --- | --- |
| `data` | `OutputData | LooseOutputData | null | undefined` | Обязательный | — | Сохранённый документ в строгом формате save() или в нестрогом формате передачи. |
| `options.schema` | `BlokViewSchema` | — | — | viewSchema из defineBlokSchema. Его единственный составленный baseSanitize — собранный из включённых СТРОЧНЫХ ИНСТРУМЕНТОВ и ТЮНОВ, а не из собственных static sanitize блочных инструментов — накладывается на стандартный строчный allowlist рендерера, поэтому строчное содержимое отображается под тем же составом, который его создал. viewSchema.tools переносится для потребителей; рендерер его не читает. Чтобы управлять разметкой собственного блока, используйте renderers. |
| `options.renderers` | `Record<string, (data, ctx) => string>` | — | — | Собственные рендереры для отдельных инструментов; такой рендерер побеждает встроенный эмиттер для своего имени инструмента. ctx даёт sanitizeInline (санитизировать строчную HTML-строку), renderBlocks (отрисовать произвольный массив блоков), plainText (простой текст HTML-строки) и renderChildren (отрисовать структурные дочерние блоки текущего блока), поэтому собственный вывод безопасно сочетается с остальным документом. |
| `options.inlineRenderers` | `Record<string, (element) => string | undefined>` | — | — | Собственные рендереры для СТРОЧНЫХ элементов с ключами по имени тега в нижнем регистре — строчный аналог renderers для меток, отображение которых не совпадает с их сохранённой разметкой (строчная формула хранит только свой исходник LaTeX, упоминание — только id). Каждый выполняется после санитизации, по пережившим её элементам, и ЗАМЕНЯЕТ элемент тем, что вернёт: undefined оставляет элемент таким, каким его оставила санитизация, '' удаляет его. Возвращённая разметка вставляется как есть — она НЕ санитизируется повторно, тот же контракт доверия, что и у вывода блочного рендерера, — поэтому она может нести разметку, которую строчный allowlist вырезал бы (например, span-элементы KaTeX). element — это { tag, attrs, html, text }. Только для отрисованного HTML: blocksToPlainText читает сохранённый исходник метки, а не её отрисовку. |
| `options.onUnknownBlock` | `'skip' | 'comment'` | — | `'skip'` | Что делать с блоком, для инструмента которого нет рендерера: молча отбросить его или оставить в выводе маркер-комментарий HTML. |
| `options.toolAttributes` | `boolean` | — | `false` | Проставлять на корне каждого блока атрибут data-blok-tool со значением <type> (для последовательностей списков — на их <ul>/<ol>) как зацепку для стилей. Подключите опциональный @bloklabs/core/view.css, чтобы воспроизвести межблочные отступы редактора из тех же токенов --blok-block-padding-*, а не восстанавливать их CSS по голым тегам. Атрибут проставляется только на встроенной разметке Blok; собственные рендереры и голые контейнеры (database) остаются нетронутыми. |
| `options.blockIds` | `boolean` | — | `false` | Проставлять на корне каждого блока атрибут data-blok-id со значением <id> (элементы списка — на своих <li>, а не на сгруппированном <ul>/<ol>), чтобы глубокие ссылки «скопировать ссылку на блок» работали и вне живого редактора. Блоки без id и голые контейнеры, которые не выдают собственного корня (database), остаются без атрибута. |
| `options.transformUrl` | `(url, ctx) => string` | — | — | Чистый хук переписывания URL, применяемый к каждому URL блока (src изображения, видео и аудио; href файла, закладки и встраивания) и к каждому href строчной ссылки — чтобы переписывать href или направлять URL изображений в CDN. ctx — это { attr: 'href' | 'src', blockType?: string } (для строчных ссылок blockType равен undefined). Он выполняется ДО вырезания небезопасных схем, поэтому переписывание никогда не сможет заново внести опасный сток javascript:/data:; возврат '' удаляет URL. |
| `options.root` | `boolean` | — | `false` | Оборачивать вывод в <div> с атрибутом data-blok-interface со значением view. Это не косметика: ограниченный областью preflight и слои токенов и цветов опираются на голый атрибут [data-blok-interface], поэтому без обёртки выдаваемые классы вычисляются иначе и @bloklabs/core/view.css не может воспроизвести внешний вид редактора. Опция включается явно, потому что добавляет элемент в существующий вывод. <BlokView> проставляет атрибут на своей собственной обёртке, поэтому потребители React её никогда не задают. |
| `options.classes` | `boolean` | — | `false` | Отрисовывать блоки с презентационными классами редактора и поблочным каркасом holder → content, чтобы результат совпадал с рендером редактора в режиме только для чтения. Чтобы это действительно отрисовалось, нужны @bloklabs/core/view.css и root: true (или предок с [data-blok-interface]); под этим флагом несколько инструментов получают ещё и элемент-обёртку. <BlokView> включает опцию по умолчанию, хук useBlokView — нет. |

TypeScript

```
import { blocksToHtml } from '@bloklabs/core/view';

const html = blocksToHtml(savedData, {
  schema: schema.viewSchema,
  onUnknownBlock: 'comment',
  renderers: {
    // Wins over the built-in paragraph emitter
    paragraph: (data, ctx) =>
      `<p class="lead">${ctx.sanitizeInline(String(data.text ?? ''))}</p>`,
  },
});
```

### blocksToPlainText(data, options?)

string

Извлечь простой текст сохранённого документа — блоки разделяются \n\n, элементы списка — \n, ячейки таблицы — \t. Синхронно и без DOM, те же опции, что у blocksToHtml, плюс includeHiddenText. Идеально для превью, поисковой индексации и подсчёта символов.

Когда использовать

В ядре нет хелпера «размер документа», потому что два размера, которые можно иметь в виду, измеряются по-разному: `blocksToPlainText(data).length` — это длина видимого содержимого (то, что напечатал пользователь), а `new TextEncoder().encode(JSON.stringify(data)).length` — размер при передаче в байтах (то, что должен проверять лимит сохранения или загрузки — например, 500KB). Используйте длину простого текста для правил по содержимому, а длину JSON в байтах — для правил по хранению.

Параметры

| Параметр | Тип | Обязательный | По умолчанию | Описание |
| --- | --- | --- | --- | --- |
| `data` | `OutputData | LooseOutputData | null | undefined` | Обязательный | — | Сохранённый документ в строгом формате save() или в нестрогом формате передачи. |
| `options.includeHiddenText` | `boolean` | — | — | Читать также медиатекст, который вывод по умолчанию опускает, потому что редактор рисует его как атрибут или не рисует вовсе: alt изображения, url видео или файла, source встраивания, title, artist и url аудиодорожки, description и url закладки. Каждое значение добавляется после видимой подписи блока, по одному в строке. По умолчанию выключено, поэтому стандартный вывод остаётся ровно тем, что читатель видит на экране, — включайте опцию для поискового индекса, где люди ищут и alt-текст, и вставленный URL. |

TypeScript

```
import { blocksToPlainText } from '@bloklabs/core/view';

// A 160-character preview for a card or meta description
const preview = blocksToPlainText(savedData).slice(0, 160);

// Content length (characters the user typed) vs transport size (bytes on the wire)
const contentLength = blocksToPlainText(savedData).length;
const transportBytes = new TextEncoder().encode(JSON.stringify(savedData)).length;
if (transportBytes > 500 * 1024) {
  throw new Error('Document exceeds the 500KB save limit');
}
```

### blocksToMarkdown(data)

string

Сериализовать сохранённый документ в Markdown — синхронно и без DOM, исходящий близнец markdownToBlocks. Заголовки становятся #, списки — -/1., задачи — - [x], таблицы — сетками из вертикальных черт в стиле GFM. В Markdown нет выноски, тоггла, колонки и отступа, поэтому выноска становится цитатой с её эмодзи, тоггл — жирной строкой-сводкой, за которой идёт его тело, колонки схлопываются в порядок чтения, а отступ отбрасывается. Возвращает '' для пустых или некорректных документов.

TypeScript

```
import { blocksToMarkdown } from '@bloklabs/core/view';

// Feed an article to an LLM, or write it to a .md file
const markdown = blocksToMarkdown(savedData);
```

### blocksToMarkdownWithReport(data)

{ markdown: string; warnings: MarkdownDegradation[] }

Тот же Markdown плюс список того, что не удалось перенести: каждая запись называет конструкцию, было ли это 'dropped' (ничего не выведено) или 'degraded' (выведено с потерями), и почему. Берите его, когда результат уходит туда, где нельзя задать уточняющий вопрос, — в AI-клиент, в экспорт — и где нужно сообщить, чего не хватает. Блок, который не даёт вывода и не несёт строчного текста, тоже попадает в отчёт, поэтому собственный инструмент без формы в Markdown называется по имени, а не исчезает.

TypeScript

```
import { blocksToMarkdownWithReport } from '@bloklabs/core/view';

const { markdown, warnings } = blocksToMarkdownWithReport(savedData);
// warnings: [{ construct: 'callout', action: 'degraded', detail: 'callout is rendered as a blockquote; …' }]
```

### htmlTextContent(html)

string

Извлечь простой текст HTML-фрагмента — синхронно и без DOM, замена element.textContent в View-рендерере. Сущности декодируются (`a &lt; b` → `a < b`), а `<br>` превращается в перевод строки. Используйте функцию вместо самодельного вырезания через DOMParser (которому нужен DOM), когда сводите строчное HTML-поле к тексту.

TypeScript

```
import { htmlTextContent } from '@bloklabs/core/view';

htmlTextContent('<b>Intro</b> &amp; more'); // → 'Intro & more'
```

### sanitizeHtmlFragment(html, config)

string

Очистить HTML-фрагмент по конфигурации санитайзера без DOM (на parse5, с той же семантикой, что у html-janitor в редакторе). `config` — это allowlist вида тег → правило либо маркер `'plaintext'`, полностью вырезающий разметку. Аналог `api.sanitizer.clean()` без DOM — используйте его в Node, воркерах и RSC, где санитайзер редактора работать не может.

TypeScript

```
import { sanitizeHtmlFragment } from '@bloklabs/core/view';

sanitizeHtmlFragment('<b>bold</b><script>x()</script>', { b: {} });
// → '<b>bold</b>'
```

### outlineFromOutputData(data)

OutlineItem[]

Извлечь структуру заголовков сохранённого документа — источник для оглавления. Синхронно и без DOM: обходит документ в порядке чтения (блоки верхнего уровня, затем структурные дочерние блоки), выбирает блоки header и сводит строчный HTML каждого заголовка к простому тексту. Каждый элемент — это { id?, level, text } — id блока задаёт якорные ссылки и цели прокрутки, поэтому отдельный проход DOMParser не нужен. Заголовки с пустым текстом пропускаются.

TypeScript

```
import { outlineFromOutputData } from '@bloklabs/core/view';

const toc = outlineFromOutputData(savedData);
// → [{ id: 'h1', level: 1, text: 'Getting Started' }, ...]
```

### restoreHeadingAnchors(data)

{ data, report }

Починить внутридокументные ссылки, чья цель потерялась при импорте. HTML адресует собственные разделы по id на заголовке (Google Docs пишет <h2> с id вида h.2y1ok8y7pef0 и ведёт своё оглавление на этот фрагмент); конвертер, который выпускает собственные id блоков и выбрасывает исходные, оставляет такие ссылки указывающими в никуда. Текст ссылки по-прежнему называет заголовок, поэтому этот проход отдаёт каждый мёртвый фрагмент тому заголовку, который назван в тексте, — как HeaderData.anchor. Поскольку проход пишет содержимое, он гадает как можно меньше: только заголовки, у которых ещё нет якоря, только точное совпадение текста (разметка, сущности и пробелы нормализуются, пунктуация — нет) и только когда ровно один заголовок и ровно один фрагмент претендуют друг на друга; всё менее надёжное остаётся нетронутым и попадает в report.skipped. Повторный запуск ничего больше не меняет. Вызывайте проход сами как разовое обновление — в автоматический путь загрузки он не входит. Без DOM, поэтому работает в Node-скрипте по сохранённым записям; сначала мигрируйте legacy-данные.

TypeScript

```
import { restoreHeadingAnchors } from '@bloklabs/core/view';

const { data, report } = restoreHeadingAnchors(savedData);
// report.restored → [{ anchor: 'h.2y1ok8y7pef0', blockId: 'header-18' }, ...]
// report.skipped  → [{ anchor: 'h.other', reason: 'ambiguous' }]
await save(data);
```

### defineBlokSchema(config)

{ editorConfig, viewSchema }

Свести конфигурацию — tools, inlineToolbar, tunes — в одну общую схему. Чистая и безопасная на уровне модуля: вызывайте её в области модуля и импортируйте результат везде. Разложите editorConfig в new Blok(...) и передайте viewSchema во view-функции — это гарантирует, что документы отображаются под ТЕМ ЖЕ составом санитизации, который их создал, а не под двумя разошедшимися конфигурациями. Гарантия действует в рамках одного состава: если вы меняете набор строчных инструментов в рантайме через tools.setInlineToolbar, пересоберите схему из текущей конфигурации. Опции, которые не участвуют в разрешении схемы (link, i18n, data, …), проходят в editorConfig без изменений.

TypeScript

```
import { defineBlokSchema, blocksToHtml } from '@bloklabs/core/view';
import { Header, Paragraph } from '@bloklabs/core/tools';

const schema = defineBlokSchema({
  tools: { paragraph: Paragraph, header: Header },
});

const editor = new Blok({ holder: 'editor', ...schema.editorConfig });
const html = blocksToHtml(savedData, { schema: schema.viewSchema });
```

### composeBaseSanitizeConfig(configs)

SanitizerConfig

Свернуть упорядоченный список конфигураций санитизации с точной семантикой слияния редактора — Object.assign, где побеждает более поздний (сначала строчные инструменты, затем тюны). Правила-функции переносятся по ссылке, а правила для одного и того же тега ЗАМЕНЯЮТСЯ и никогда не сливаются вглубь. Это та же свёртка, которой defineBlokSchema строит viewSchema.baseSanitize, открытая для списков, собранных вручную. Экспортируется и из @bloklabs/core, и из @bloklabs/core/view.

TypeScript

```
import { composeBaseSanitizeConfig } from '@bloklabs/core/view';

const baseSanitize = composeBaseSanitizeConfig([
  { b: {}, i: {} },
  { a: { href: true } },
]);
// → { b: {}, i: {}, a: { href: true } }
```

### blocksToViewNodes(data, options?)

ViewNode[]

Отрисовать в независимое от фреймворка JSON-дерево вместо строки HTML: каждый узел — это { tag, attrs, children } или { text }, с теми же опциями и конвейером санитизации, что у blocksToHtml. Именно это React-биндинги превращают в настоящие элементы. Экспериментально — форма не зафиксирована, пока её не начнёт использовать второй адаптер фреймворка, поэтому она может измениться в минорном релизе.

TypeScript

```
import { blocksToViewNodes } from '@bloklabs/core/view';

const nodes = blocksToViewNodes(savedData);
// → [{ tag: 'p', attrs: {}, children: [{ text: 'Hello' }] }]
```

### renderLatex(latex, options?)

Promise<string>

Отрисовать строку LaTeX в HTML той сборкой KaTeX, которую Blok уже включает в бандл (её используют и инструмент кода, и строчный инструмент формул, и импортёр Markdown), с усилением для недоверенного ввода: trust: false запрещает команды, внедряющие разметку (\href, \includegraphics, \html*), maxExpand ограничивает раскрытие макросов, maxSize ограничивает размеры элементов, а throwOnError: false отрисовывает некорректную формулу как экранированный исходник, вместо того чтобы сломать документ. Берите её вместо того, чтобы добавлять katex собственной зависимостью, — это вторая копия библиотеки и второй, непроверенный набор опций. KaTeX импортируется лениво при первом вызове; если документа нет (SSR, воркеры), вставка таблицы стилей пропускается и katex.min.css вы подключаете сами, а разметка получается идентичной. Для inlineRenderers используйте createLatexRenderer — он синхронный.

TypeScript

```
import { renderLatex } from '@bloklabs/core/view';

const html = await renderLatex('c = \\pm\\sqrt{a^2 + b^2}', { displayMode: false });
```

### createLatexRenderer()

Promise<(latex: string, options?: LatexRenderOptions) => string>

Загрузить KaTeX один раз и получить обратно синхронный рендерер LaTeX — ту форму, которая нужна inlineRenderers, ведь он заменяет элемент строкой, которую тот вернул, а промис там превращается в [object Promise]. Устроено как «дождитесь загрузчика — получите рендерер», поэтому в порядке вызовов невозможно ошибиться: рендерер не может появиться раньше, чем готов KaTeX. Опции усилены так же, как в renderLatex. Именно это позволяет отображающей поверхности отрисовывать формулы через blocksToHtml/BlokView, а не поднимать ради них редактор в режиме только для чтения.

TypeScript

```
import { blocksToHtml, createLatexRenderer } from '@bloklabs/core/view';

const renderLatexSync = await createLatexRenderer();

const html = blocksToHtml(savedData, {
  inlineRenderers: {
    span: ({ attrs }) => attrs['data-latex'] === undefined
      ? undefined
      : renderLatexSync(attrs['data-latex'], { displayMode: false }),
  },
});
```

### BlokView

ReactNode

React-компонент отображения из @bloklabs/react: отрисовывает сохранённый документ внутри одной <div>-обёртки — без экземпляра редактора, без обрамления, без асинхронности, без эффектов и никогда через dangerouslySetInnerHTML (содержимое переносится из санитизированного view-дерева в настоящие React-элементы). Обёртка всегда несёт data-blok-interface со значением view — именно поэтому выдаваемые классы вычисляются так же, как в редакторе; атрибут пишется ДО разложения divProps, поэтому вызывающий код может его переопределить — но никогда на «blok», который несёт all: initial !important и заблокировал бы типографику хоста. Это очевидный путь «только для чтения» — берите его вместо <BlokEditor readOnly> там, где нужно лишь отображение: он не стоит редакторского бандла, у него нет защёлки готовности, и он одинаково отрисовывается под SSR. Его API пропсов стабилен; экспериментальным остаётся только сырое дерево ViewNode, из которого он строит вывод (через blocksToViewNodes), и использование BlokView вас с ним никогда не сталкивает.

Параметры

| Параметр | Тип | Обязательный | По умолчанию | Описание |
| --- | --- | --- | --- | --- |
| `data` | `OutputData | LooseOutputData | null | undefined` | Обязательный | — | Сохранённый документ для отображения (nullish допускается). |
| `schema` | `BlokViewSchema` | — | — | viewSchema из defineBlokSchema — отображение под тем составом, который создал документ. |
| `renderers` | `Record<string, (data, ctx) => string>` | — | — | Собственные рендереры для отдельных инструментов; побеждают встроенные. |
| `onUnknownBlock` | `'skip' | 'comment'` | — | `'skip'` | Политика для неизвестных инструментов (маркеры 'comment' в React-дереве отбрасываются). |
| `toolAttributes` | `boolean` | — | `false` | Проставлять data-blok-tool на корне каждого блока (работает в паре с @bloklabs/core/view.css). Передаётся в blocksToHtml. |
| `blockIds` | `boolean` | — | `false` | Проставлять data-blok-id на корне каждого блока (элементы списка — на своих <li>) для глубоких ссылок «скопировать ссылку на блок». |
| `transformUrl` | `(url, ctx) => string` | — | — | Хук переписывания URL для URL блоков и строчных ссылок, выполняется до вырезания небезопасных схем. Передаётся в blocksToHtml. |
| `inlineRenderers` | `Record<string, (element) => string | undefined>` | — | — | Рендереры по тегам для СТРОЧНЫХ элементов — отрисовать на сервере LaTeX формулы через KaTeX, превратить span упоминания в чип. Передаётся в blocksToHtml. |
| `classes` | `boolean` | — | `true` | Отрисовывать с презентационными классами редактора и поблочным каркасом holder → content, чтобы вывод совпадал с рендером редактора в режиме только для чтения. Здесь включено по умолчанию — компонент владеет обёрткой, и его задача — выглядеть как редактор, — а чтобы это отрисовалось, нужен подключённый @bloklabs/core/view.css. Передайте classes={false} для семантической разметки без стилей. |
| `...divProps` | `HTMLAttributes<HTMLDivElement>` | — | — | Любой стандартный атрибут <div> (className, id, style, data-*, aria-*, обработчики событий, …) передаётся на единственный элемент-обёртку. |

TypeScript

```
import { BlokView } from '@bloklabs/react';
import { schema } from './schema';
import '@bloklabs/core/view.css'; // opt-in block-spacing baseline

export function Article({ saved }: { saved: OutputData }) {
  return (
    <BlokView
      data={saved}
      schema={schema.viewSchema}
      toolAttributes
      blockIds
      id="article-body"
      className="prose"
    />
  );
}
```

### useBlokView(data, options?)

ReactNode

Форма BlokView без обёртки: возвращает Fragment из элементов блоков, без лишнего <div>, для мест, где обёртка недопустима или нежелательна: подписей чекбоксов, ячеек таблицы, заголовков. Синхронно и без эффектов (безопасно для SSR), с мемоизацией по ссылке на data и по отдельным значениям опций. Опции те же, что у blocksToHtml, кроме root — хук её игнорирует, ведь не выдавать обёртку и есть его контракт — и classes, которая здесь по умолчанию false (в <BlokView>, владеющем обёрткой, по умолчанию true). Передача root: true проходит проверку типов и молча ничего не делает; когда нужно, чтобы отрисовался view.css, оберните возвращённый Fragment сами в элемент с атрибутом data-blok-interface со значением view.

TypeScript

```
import { useBlokView } from '@bloklabs/react';

function RowLabel({ saved }: { saved: OutputData }) {
  const content = useBlokView(saved, { schema: schema.viewSchema });
  return <label>{content}</label>;
}
```

TypeScript

```
// schema.ts — pure and module-scope-safe; share it between editor and server
import { defineBlokSchema } from '@bloklabs/core/view';
import { Header, Paragraph, List } from '@bloklabs/core/tools';

export const schema = defineBlokSchema({
  tools: { paragraph: Paragraph, header: Header, list: List },
});

// Editing side (browser)
import Blok from '@bloklabs/core';
const editor = new Blok({ holder: 'editor', ...schema.editorConfig });

// Display side — Node, a worker, an RSC, or the browser; no DOM needed
import { blocksToHtml, blocksToPlainText } from '@bloklabs/core/view';
const html = blocksToHtml(savedData, { schema: schema.viewSchema });
const preview = blocksToPlainText(savedData).slice(0, 160);
```
