Tiptap: текстовый редактор JavaScript — 8 сценариев и мастерская
Собираем удобный текстовый редактор на JavaScript. Восемь живых сценариев Tiptap, локальный черновик, таблицы, ограничения, экспорт и готовый проект для Windows, Linux и VPS.

Tiptap нужен там, где обычного поля textarea уже мало: в описании задачи, ответе клиенту, внутренней инструкции, карточке товара или заметке к заказу. Пользователь хочет выделить важную фразу, добавить список, отметить выполненные пункты и вставить небольшую таблицу. Разработчику при этом нужны предсказуемые данные, собственный дизайн и контроль над возможностями редактора.
В этой статье соберём такой интерфейс на обычном JavaScript. На странице работает мини-мастерская с восемью задачами: можно писать, менять структуру документа, работать с таблицами, сохранять черновик на устройстве и скачивать результат. Это настоящий Tiptap 3.33.0, собранный из npm-пакетов. В архиве есть весь интерфейс, исходники, фиксированные версии зависимостей, тесты и инструкция для Windows, Linux и отдельного сайта на VPS.
Что такое Tiptap и что получает пользователь
Tiptap — набор инструментов для создания редакторов форматированного текста. Под ним работает ProseMirror, который отвечает за модель документа и операции над ней. Tiptap предоставляет расширения и удобные команды: включить список, применить жирное начертание, вставить таблицу, получить JSON. Кнопки, расположение панели, подсказки и логику сохранения вы определяете сами.
Это особенно удобно в прикладном продукте. Команде поддержки не нужна половина возможностей офисного процессора: достаточно абзацев, списка и ссылки. В трекере задач полезны заголовки, цитата и блок кода. В базе знаний требуется структура длинной инструкции. Во всех случаях можно использовать одну технологию, но показать пользователю ровно тот набор действий, который помогает выполнить его работу.
Редактор не требует React или Vue. В нашей мастерской HTML5 отвечает за разметку, Bootstrap5 даёт базовые элементы интерфейса, собственный CSS оформляет документ, а JavaScript связывает действия пользователя с экземпляром Editor. Готовая сборка работает как статический сайт: PHP или Node.js для самого ввода текста не нужны. Сервер понадобится, когда вы захотите хранить общие документы, проверять права и синхронизировать изменения.
Используются бесплатные пакеты под MIT. В Tiptap есть также отдельные платные продукты, облачные сервисы и расширения. Эта статья их не требует: аккаунт, ключ API и подписка не нужны. Совместное редактирование, комментарии пользователей и серверная история версий здесь не демонстрируются. Их подключение — отдельная архитектурная задача, а не результат установки нескольких кнопок.
Источник: официальное описание редактора.
Когда достаточно textarea, а когда нужен редактор
Для номера заявки, поискового запроса, короткой подписи или служебного комментария без форматирования обычное поле часто удобнее. Оно проще, предсказуемее копируется и не требует поддерживать схему документа. Добавлять богатый редактор только ради внешнего вида означает усложнять интерфейс, не помогая человеку решить задачу.
Tiptap оправдан, когда структура текста несёт смысл. Заголовки позволяют ориентироваться в инструкции, список отделяет шаги процесса, ссылка связывает объяснение с источником, а таблица помогает сравнить варианты. Если пользователи постоянно пишут «важно» заглавными буквами и рисуют списки дефисами, это хороший повод изучить реальный сценарий и дать подходящие инструменты.
Самостоятельный contenteditable подходит для эксперимента, но быстро требует обработки выделения, истории, вставки, вложенности и одинакового поведения команд. Готовая модель документа снимает большую часть этой работы. При этом она не освобождает от проектирования продукта: кто редактирует, где хранится результат и как объясняется ошибка, всё равно решает ваше приложение.
Мини-мастерская: восемь интерфейсов на одной странице
Выберите задачу слева или в сетке над редактором. У каждого сценария свой документ и набор разрешённых возможностей. Это видно не только по кнопкам: например, в коротком сообщении нет узлов таблицы и заголовка, а в сравнении тарифов подключены настоящие табличные расширения. Ограниченная схема помогает удерживать формат документа в рамках продукта.
Попробуйте сначала изменить текст, затем переключить сценарий и вернуться. Изменения останутся в памяти открытой страницы. Они не отправляются автору сайта и не исчезают при каждом переключении. Обновление страницы очищает эту рабочую сессию. Для сохранения после перезагрузки предназначен отдельный сценарий «Локальный черновик», где запись в хранилище выполняется только явной кнопкой.
Под документом находятся вкладки JSON, HTML и JavaScript. Первые две показывают текущий результат, включая ваши изменения. JavaScript содержит рецепт инициализации с текущим содержимым и настройками сценария. Его используют вместе с editor.js из архива. Можно скопировать результат или скачать файл. HTML в панели показывается как текст, поэтому просмотр исходника не запускает разметку из поля ввода.
Внутри встроенной мастерской можно прокручивать страницу. На телефоне удобнее открыть её на весь экран.
Как устроен документ: узлы, отметки и схема
Редактор хранит не просто строку с тегами. Документ представляет собой дерево: внутри doc находятся абзацы, заголовки, списки или таблицы. Текст может иметь marks — например, жирное начертание или ссылку. Вложенность описывает смысл: строка таблицы содержит ячейки, пункт чек-листа содержит текст и признак checked.
Такое представление полезно при обработке данных. Можно подсчитать выполненные пункты, извлечь ссылки, показать короткую текстовую выдержку или проверить наличие обязательного раздела. Не требуется искать нужные места регулярными выражениями по HTML. При этом JSON сам по себе не гарантирует корректность: сервер всё равно должен проверять допустимые типы узлов, атрибуты и размер.
Схема определяется подключёнными расширениями. Если сохранить документ с таблицей, а потом открыть его редактором, который таблицы не знает, нельзя рассчитывать на полное восстановление. Поэтому формат документа должен развиваться вместе с кодом приложения. В реальном хранилище стоит записывать schemaVersion, а при изменении схемы заранее продумывать миграцию существующих записей.
const document = {
type: 'doc',
content: [{
type: 'paragraph',
content: [{ type: 'text', text: 'Важная мысль', marks: [{ type: 'bold' }] }],
}],
};
Источник: Nodes and Marks.
Первый редактор на обычном JavaScript
Для начала достаточно контейнера, экземпляра Editor и StarterKit. Этот набор включает распространённые узлы и форматирование. В третьей версии StarterKit уже есть Link и Underline, поэтому их нельзя бездумно подключать второй раз под теми же именами. Нужные параметры можно передать через configure, а лишние возможности отключить значением false.
Установка выполняется через npm, затем сборщик объединяет импорты в браузерный файл. В нашем проекте используется esbuild: на сайте загружается локальный app.js, а не цепочка внешних CDN. Lockfile фиксирует весь набор зависимостей. После обновления пакетов нужно заново собрать файл и проверить сценарии, особенно восстановление старого JSON.
import { Editor } from '@tiptap/core';
import StarterKit from '@tiptap/starter-kit';
const editor = new Editor({
element: document.querySelector('#editor'),
extensions: [StarterKit.configure({ heading: { levels: [2, 3] } })],
content: '<p>Начните с понятного сообщения.</p>',
});
// Когда интерфейс больше не нужен:
// editor.destroy();
Документация: Vanilla JavaScript, состав StarterKit.
1. Сообщение клиенту: минимум действий, понятный результат
Первый сценарий подходит для CRM, ответа службы поддержки или комментария к заказу. Здесь человеку важнее быстро сформулировать мысль, чем выбирать шрифт и цвет. Мы оставляем выделение, список и ссылку, убираем таблицы и сложные блоки, ограничиваем текст полутора тысячами символов. Небольшой набор инструментов уменьшает вероятность случайно испортить оформление.
Выделите фразу о сроке ответа и нажмите «Жирный». Команда возвращает фокус в редактор, затем применяет форматирование к сохранённому выделению. Нажатие «Отменить» возвращает предыдущий документ; «Повторить» восстанавливает действие. Переключите результат на JSON и найдите mark bold: пользователь видит выделенную фразу, приложение получает структурированную отметку.
В готовом сервисе рядом с редактором появится кнопка отправки. Перед отправкой нужно проверить адресата, права сотрудника и фактический текст. Пустой визуальный документ может содержать технический абзац, поэтому проверка длины HTML не равна проверке содержания. Используйте свойства редактора и отдельные бизнес-правила. Здесь отправки нет: вы экспериментируете с текстом, который остаётся в браузере.
Для email итоговый HTML обычно проходит дополнительную подготовку: почтовые клиенты имеют собственные ограничения на стили и разметку. Полученный фрагмент удобен как исходный материал, но редактор не является почтовым транспортом и не гарантирует одинаковое отображение во всех почтовых программах.
editor.chain().focus().toggleBold().run();
const text = editor.getText();
const document = editor.getJSON();
if (editor.isEmpty || !text.trim()) {
console.log('Сначала напишите сообщение');
}
2. Описание задачи: цель, критерии и технические детали
В трекере задач длинный неструктурированный комментарий быстро становится проблемой. Исполнителю приходится искать, что сделать, какие ограничения учитывать и по чему проверить готовность. Заголовки разделяют эти части, список фиксирует критерии, блок кода сохраняет команду или маршрут API без смешивания с обычным текстом.
В мастерской откройте «Описание задачи». Измените заголовок, допишите один критерий и поставьте курсор в технический блок. Форматирование работает над текущим блоком, поэтому для заголовка достаточно положения курсора, а для выделения отдельной фразы нужно выделить текст. Это различие стоит объяснять в своём интерфейсе через названия и состояния кнопок.
Такой редактор можно встроить в карточку заявки, техническое задание или запись о дефекте. Хороший шаблон направляет автора, но не заменяет проверку бизнес-правил. Если для передачи задачи обязательно нужны срок, ответственный и приоритет, лучше хранить их отдельными полями формы. Свободный текст удобен для объяснения, а структурированные поля — для фильтров, уведомлений и отчётов.
В нашем примере блок кода не выполняется. Он сохраняет текст команды как часть документа. Это позволяет безопасно показывать технические инструкции без ложного обещания, что редактор способен запускать Node.js, PHP или консоль. Подсветка синтаксиса тоже не возникает автоматически: для неё подключают подходящее расширение и стили.
editor.chain().focus().toggleHeading({ level: 2 }).run();
editor.chain().focus().toggleCodeBlock().run();
3. Чек-лист: состояние пункта становится данными
Чек-лист полезен при выпуске страницы, приёмке работы, подготовке документа или повторяющейся процедуре. Здесь важно не только перечислить действия, но и отметить выполнение. TaskList и TaskItem дают такой формат внутри текста: рядом с пунктом появляется флажок, а в JSON сохраняется булево checked.
Отметьте несколько пунктов и наблюдайте за строкой прогресса. Она вычисляется из текущего документа, а не увеличивается отдельным счётчиком при каждом клике. Благодаря этому восстановление JSON сразу возвращает правильное количество выполненных задач. Если пользователь добавит новый пункт или отменит действие, прогресс останется согласованным с текстом.
Поставьте курсор в конец пункта и нажмите Enter. Редактор создаст следующий элемент того же списка. Флажки имеют достаточно большую область нажатия, чтобы ими можно было пользоваться на телефоне. В режиме просмотра изменение флажков запрещено вместе с редактированием документа: просмотр не должен незаметно менять сохранённые данные.
У такого чек-листа есть граница. Он подходит для личного плана или содержания инструкции. Если каждый пункт должен иметь исполнителя, срок, историю действий и независимые права, возможно, правильнее сделать отдельную сущность Task в базе, а в редакторе оставить ссылку на неё. Состояние внутри документа не заменяет полноценный трекер задач.
import { TaskList, TaskItem } from '@tiptap/extension-list';
const extensions = [
StarterKit,
TaskList,
TaskItem.configure({ nested: true }),
];
// Добавьте extensions в new Editor({ ... }).
Источник: TaskList.
4. Таблица: сравнение внутри текста
Небольшая таблица удобна в предложении клиенту, инструкции или описании комплекта услуг. В ней можно сопоставить тариф, поддержку и цену, сохранив объяснение рядом. Для этого подключён TableKit, который содержит таблицу, строки и ячейки. Это редактируемая структура документа, а не нарисованная сетка поверх текста.
Поставьте курсор в ячейку и добавьте строку. Новая строка появляется относительно текущей, поэтому действие имеет понятный контекст. Затем добавьте столбец и отмените изменение. Если курсор находится вне таблицы, мастерская объяснит, почему команда неприменима. Такие сообщения полезнее кнопки, которая просто ничего не делает.
На узком экране таблица сохраняет минимальную ширину и прокручивается внутри своей области. Мы не пытаемся сжать каждую ячейку до нескольких букв. Панель инструментов переносится строками, а сама страница остаётся в ширине экрана. Это особенно важно для редактора, встроенного в iframe статьи: у него может быть меньше трёхсот пикселей полезной ширины.
Таблица редактора не является электронной таблицей. Здесь нет формул, типов столбцов и пересчёта итогов. Если нужны сортировка тысяч строк, фильтры, массовое редактирование или вычисления, берите специализированный компонент таблицы данных. Tiptap уместен, когда таблица является частью человеческого объяснения и её размер остаётся небольшим.
import { TableKit } from '@tiptap/extension-table';
// В extensions:
TableKit.configure({ table: { resizable: false, renderWrapper: true } });
// После создания Editor:
editor.commands.insertTable({ rows: 3, cols: 3, withHeaderRow: true });
editor.commands.addRowAfter();
Источник: Table и табличные команды.
5. База знаний: ссылки с понятной политикой
Внутренняя инструкция обычно соединяет несколько источников: описание процесса, форму заявки, документацию и примеры. Редактор позволяет оформить эту связь без ручного написания HTML. Выделите название ресурса, введите полный адрес и примените ссылку. Выделение сохраняется, пока фокус находится в поле URL.
Мы разрешаем только абсолютные HTTP и HTTPS-адреса без встроенного логина и пароля. javascript:, data: и адреса с управляющими символами отклоняются. Политика применяется и в интерфейсе, и в конфигурации Link. Это важное разделение: проверка только в поле URL не покрывает вставку, восстановление документа и другие пути появления ссылки.
В режиме редактирования клик по ссылке не уводит человека со страницы. Ему нужно поставить курсор и изменить подпись. Если вашему продукту нужен переход, добавьте отдельное действие «Открыть ссылку», проверьте адрес и откройте его с подходящим rel. Не смешивайте редактирование ссылки с неожиданной навигацией по обычному нажатию.
Даже допустимый HTTPS-адрес не является знаком доверия к содержимому сайта. Проверка схемы защищает от опасных способов выполнения кода, но не определяет правдивость источника. Для корпоративной базы можно дополнительно ограничить домены или показать пользователю предупреждение о внешнем ресурсе, если этого требует конкретный сценарий.
StarterKit.configure({
link: {
openOnClick: false,
autolink: false,
linkOnPaste: false,
isAllowedUri: url => Boolean(safeURL(url)),
},
});
// safeURL находится в src/editor.js проекта.
Источник: Link и isAllowedUri.
6. Локальный черновик: сохранить и продолжить
Черновик нужен, когда пользователь отвлёкся, случайно обновил страницу или хочет вернуться к заметке позже. Для демонстрации не требуется сервер: документ можно сохранить в localStorage того же браузера. В мастерской это сделано явной кнопкой, чтобы посетитель понимал, когда данные записываются на устройство.
Измените заметку, сохраните её, обновите страницу и вернитесь в этот сценарий. Нажмите «Восстановить»: появится сохранённый документ. Сама перезагрузка не подставляет его автоматически, поэтому пример не скрывает состояние хранилища. Удаление копии очищает localStorage, но оставляет текущий текст в редакторе — это два разных состояния, и интерфейс прямо об этом сообщает.
Вместе с документом сохраняется версия формата и время записи. При чтении проверяются JSON, версия, структура, размер, глубина и ссылки. Повреждённое хранилище не должно разрушать интерфейс: пользователь получает ошибку, а текущий документ остаётся доступным. Запись тоже может не сработать из-за ограничений браузера или переполнения, поэтому предусмотрено скачивание JSON.
localStorage привязан к origin: протоколу, хосту и порту. Черновик с localhost:8103 не появится автоматически на другом домене или в другом браузере. Это не резервное копирование и не синхронизация. На общем компьютере данные доступны другим людям с доступом к профилю; пароли, токены и чувствительные материалы сюда вводить не следует.
const saved = {
version: 1,
savedAt: new Date().toISOString(),
document: editor.getJSON(),
};
try {
localStorage.setItem('my-editor:draft:v1', JSON.stringify(saved));
} catch {
// Покажите ошибку и предложите скачать JSON.
}
7. Короткий анонс: ограничение как часть интерфейса
Текст для карточки, уведомления или краткого описания должен помещаться в отведённое место. Если обнаружить превышение только при отправке, человек потратит время на сокращение уже написанного. CharacterCount позволяет показать счётчик сразу и ограничить документ выбранным числом символов.
В сценарии установлен предел 180. Допишите анонс или заполните поле до лимита специальной кнопкой. Когда место закончится, ввод, увеличивающий размер, будет ограничен. Удаление текста остаётся доступным. Вставка большого фрагмента тоже проходит правила расширения; не рассчитывайте, что всё вставленное обязательно сохранится целиком.
Здесь символ считается через Intl.Segmenter по графемам. Это ближе к восприятию человека: составной emoji может занимать несколько кодовых единиц JavaScript, но выглядеть одним знаком. Выбранное правило важно согласовать с сервером и внешним каналом. Ограничение SMS, байтовый лимит базы и размер JSON — разные вещи, их нельзя бездумно заменить одним text.length.
Счётчик показывает техническое ограничение, но не оценивает качество текста. Рядом полезно объяснить назначение поля и дать пример. Сервер повторно проверяет допустимую длину, потому что клиентский JavaScript можно изменить или обойти. Ограничение в редакторе повышает удобство, а серверная проверка защищает контракт приложения.
import { CharacterCount } from '@tiptap/extensions';
CharacterCount.configure({
limit: 180,
autoTrim: false,
textCounter: text => [...new Intl.Segmenter('ru', {
granularity: 'grapheme',
}).segment(text)].length,
});
Источник: CharacterCount.
8. Просмотр и экспорт: один документ, несколько представлений
Последний сценарий открывается в режиме чтения. Панель форматирования скрыта, текст нельзя изменить, а JSON и HTML доступны как результат. Нажмите «Редактировать», внесите правку и вернитесь в просмотр. Меняется возможность редактирования одного экземпляра, а не создаётся независимая копия, которая могла бы отстать от исходника.
Такой подход полезен в карточке документа: сотрудник читает содержание и включает редактирование только при необходимости. В реальном продукте право на изменение приходит с сервера. Скрытая кнопка и editable:false сами по себе не запрещают отправить запрос напрямую, поэтому API обязан повторно проверить пользователя и документ.
JSON удобен для повторного открытия в редакторе и анализа структуры. HTML полезен для показа в другом интерфейсе. В мастерской HTML скачивается как фрагмент, а JavaScript — как интеграционный рецепт с текущей конфигурацией. Внутри рецепта используется функция createEditor из архива: это помогает перенести одинаковую настройку расширений, вместо того чтобы незаметно потерять часть правил.
Экспорт getHTML() не включает оформление всей страницы. Шрифты, отступы, цвета таблиц и чек-листов задаются стилями вашего продукта. Если HTML пойдёт в другой сервис, нужно перенести совместимую часть CSS и проверить итоговое отображение. Для внешнего или пользовательского HTML необходим отдельный продуманный процесс очистки перед выводом.
editor.setEditable(false);
const json = editor.getJSON();
const html = editor.getHTML();
const plainText = editor.getText();
// Для показа исходника:
document.querySelector('#source').textContent = html;
Источник: экспорт JSON и HTML.
Как подключить сохранение в свой сервис
Начните с границ документа: кому он принадлежит, кто может читать и кто может менять. API получает не только JSON, но и идентификатор записи, версию схемы и ожидаемую ревизию. Сервер сверяет права, проверяет размер и структуру, затем сохраняет новую версию. Проверка ревизии помогает заметить, что другой человек уже обновил запись.
Автосохранение требует видимого состояния: «есть изменения», «сохраняем», «сохранено» и «ошибка». Сохранять на каждый ввод без задержки обычно избыточно. Используйте debounce и отслеживайте порядок ответов: поздний ответ старого запроса не должен затереть более свежий текст или показать ложное «сохранено». При потере сети оставьте возможность повторить запрос и экспортировать документ.
Если нужно одновременное редактирование, простая отправка последнего JSON не умеет корректно объединять изменения. Для этого существуют CRDT-подходы и интеграции с Yjs/Hocuspocus, однако им нужны собственные хранение, авторизация и проверка прав подключения. Не обещайте совместную работу, пока эти части не реализованы и не проверены.
В PHP-проекте Tiptap остаётся браузерным компонентом, а PHP принимает и выдаёт данные через ваш API. Во Vue или React меняется интеграционный слой, но вопросы схемы, прав и хранения остаются теми же. Выбор фреймворка не снимает необходимость проверять входящий документ на сервере.
Ошибки, безопасность и обслуживание редактора
Самые частые ошибки начинаются с предположения, что редактор уже решает все задачи приложения. Он не хранит данные в базе, не проверяет роль пользователя и не гарантирует безопасность произвольного HTML. Схема ограничивает поддерживаемый документ, но её нельзя объявлять универсальным XSS-фильтром для любого контента из сети.
Не подставляйте чужую строку через innerHTML только потому, что она пришла из поля «редактор». Определите разрешённые элементы, атрибуты и схемы ссылок, используйте поддерживаемый sanitizer на соответствующей границе. Если храните JSON, проверяйте его перед рендерингом той же согласованной схемой. Не доверяйте данным из localStorage больше, чем данным HTTP-запроса.
При удалении редактора из интерфейса вызывайте destroy(). Это освобождает обработчики и связанные объекты. В мастерской предыдущий экземпляр уничтожается при смене сценария, а содержимое остаётся отдельно в Map. Если встроить редактор в модальное окно или динамическую страницу, нужно столь же явно связать его жизненный цикл с жизненным циклом интерфейса.
Не пересоздавайте Editor при каждом нажатии клавиши. Это сбрасывает выделение, мешает истории и может приводить к утечкам. Обновляйте только нужные показатели, а сохранение организуйте отдельно. Крупные документы, вложенные таблицы и сложные расширения проверяйте на реальных объёмах: комфортный небольшой пример не доказывает производительность редактора на сотнях страниц.
Доступность и мобильный интерфейс
Кнопкам нужны понятные подписи и доступное состояние. Для форматирования в мастерской используется aria-pressed; вкладками результата можно управлять стрелками, Home и End. После команды фокус возвращается в редактор, а после смены сценария — на заголовок нового примера. Человек должен понимать, где находится и что сейчас изменилось.
Поле текста имеет роль textbox и признак многострочного ввода. Панель переносится, элементы управления достаточно крупные для касания, а таблица получает собственную горизонтальную прокрутку. В узком iframe важен каждый уровень вложенных отступов: даже хороший внешний макет может оставить редактору слишком мало пространства.
Автоматические DOM-тесты полезны для команд и состояния, но не заменяют проверку выделения мышью, экранной клавиатуры, прокрутки и мобильного Safari. Отдельно проверьте вставку текста, undo/redo, переход между полем ссылки и документом, длинные слова, таблицу и восстановление черновика. Для анимаций учитывается prefers-reduced-motion; само редактирование не должно зависеть от эффектов.
Windows: локальная работа с нуля
Установите Node.js 24 LTS из официального MSI: https://nodejs.org/en/download/archive/v24. Откройте новый PowerShell после установки. Распакуйте архив, перейдите в каталог, где лежит package.json:
Set-Location C:\projects\tiptap-project
node --version
npm.cmd ci
npm.cmd run build
npm.cmd test
npm.cmd start
Откройте http://127.0.0.1:8103/. В другом окне:
(Invoke-WebRequest http://127.0.0.1:8103/).StatusCode
(Invoke-WebRequest http://127.0.0.1:8103/app.js).Headers['Content-Type']
Ожидаются 200 и text/javascript; charset=utf-8. Сервер останавливается Ctrl+C. Если порт занят, перед стартом задайте $env:PORT='8104' и откройте соответствующий адрес. Сборку повторяют после изменения src/; сервер отдаёт файлы без кеширования. file:// для проверки не используйте: HTTP обеспечивает нормальную работу модулей и предсказуемое хранилище.
Linux: локальный запуск
Нужен Node.js 24. На Ubuntu/Debian можно использовать приложенный установщик официального архива. Он проверяет SHA256, поддерживает x64/arm64, повторно использует рабочий Node24 в /opt/node24 и отказывается перезаписывать другой существующий каталог. Не запускайте npm от root.
cd ~/projects/tiptap-project
sudo bash deploy/install-node24.sh
export PATH="/opt/node24/bin:$PATH"
node --version
npm ci
npm run build
npm test
npm start
Во втором терминале:
curl --fail -I http://127.0.0.1:8103/
curl --fail -I http://127.0.0.1:8103/app.js
Ожидайте HTTP 200 и JavaScript MIME. Порт меняется через PORT=8104 npm start. Это сервер предпросмотра для одного разработчика; для публичного домена используйте статическую выдачу Caddy ниже.
Ubuntu 24.04 VPS: подготовка и сборка
Понадобятся SSH-доступ с sudo, домен и A/AAAA-записи на адрес сервера. Откройте TCP 80/443 в сетевом экране провайдера; SSH-доступ сохраните. Если AAAA-запись есть, IPv6 тоже должен вести на этот сервер. Для сборки npm нужен исходящий интернет; готовая мастерская затем работает без него.
Команды рассчитаны на отдельный новый сайт. Не заменяйте конфигурацию уже работающего Caddy целиком: добавьте блок домена в существующую конфигурацию и проверьте её.
sudo apt-get update
sudo apt-get install -y curl unzip ca-certificates
mkdir -p "$HOME/tiptap-build"
cd "$HOME/tiptap-build"
curl -fL -o tiptap-project.zip \
https://ae-nekrasov.ru/wp-content/plugins/aen-tiptap-workshop/tiptap-project.zip
unzip tiptap-project.zip
cd tiptap-project
sudo bash deploy/install-node24.sh
export PATH="/opt/node24/bin:$PATH"
npm ci
npm run build
npm test
Сборка выполняется от SSH-пользователя. Исходники, node_modules и тесты не попадают в веб-каталог: публикуется только public. У этой мастерской нет серверной базы, API и переменных с секретами.
Размещение статики и HTTPS
RELEASE="$(date -u +%Y%m%dT%H%M%SZ)"
sudo install -d -m 755 /srv/tiptap/releases
sudo cp -a public "/srv/tiptap/releases/$RELEASE"
sudo chown -R root:root "/srv/tiptap/releases/$RELEASE"
sudo find "/srv/tiptap/releases/$RELEASE" -type d -exec chmod 755 {} +
sudo find "/srv/tiptap/releases/$RELEASE" -type f -exec chmod 644 {} +
sudo ln -sfn "/srv/tiptap/releases/$RELEASE" /srv/tiptap/current
sudo bash deploy/install-caddy.sh
sudo cp -a /etc/caddy/Caddyfile /etc/caddy/Caddyfile.before-tiptap
sudo install -m 644 deploy/Caddyfile.example /etc/caddy/Caddyfile
sudo nano /etc/caddy/Caddyfile
Замените editor.example.com на свой реальный домен. На сервере с другими сайтами объедините блоки вручную, сохранив их. Затем:
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl enable --now caddy
sudo systemctl reload caddy
sudo systemctl status caddy --no-pager
curl --fail -I https://editor.example.com/
curl --fail -I https://editor.example.com/app.js
sudo journalctl -u caddy -n 50 --no-pager
Ожидаются HTTP 200 и JavaScript MIME. Сертификат получит Caddy после правильной настройки DNS и портов. Веб-сервер автоматически запускается после перезагрузки. Node.js после сборки не нужен для обслуживания посетителей, отдельная Node-служба не создаётся. Корень public — статический, доступ к исходникам закрыт размещением за его пределами.
Закрытый редактор для сотрудников
Публичная демка не хранит документы на сервере. Если редактор предназначен для сотрудников или позже получает API с рабочими документами, сначала ограничьте доступ. Для небольшой закрытой демонстрации можно включить Caddy Basic Auth поверх HTTPS. Хеш пароля создаётся интерактивно:
sudo caddy hash-password
sudo nano /etc/caddy/Caddyfile
Добавьте внутрь блока домена, сохранив остальные директивы:
basic_auth {
editor REPLACE_WITH_GENERATED_PASSWORD_HASH
}
Подставьте полученный хеш; не храните открытый пароль в файле. Проверьте конфигурацию и перезагрузите Caddy. curl -I https://editor.example.com/ теперь вернёт 401; curl --user editor -I https://editor.example.com/ запросит пароль и вернёт 200. Basic Auth не заменяет проверку прав в будущем API. localStorage остаётся доступным скриптам того же origin и людям с доступом к браузеру.
Обновление, откат и журналы
Перед изменениями сохраните собственные исходники и lockfile. Для обновления пакетов создайте отдельную копию проекта, осознанно обновите версии, выполните npm ci/build/test и проверьте документы старой схемы. Для нового релиза статических файлов:
npm ci
npm run build
npm test
PREVIOUS="$(readlink -f /srv/tiptap/current)"
RELEASE="$(date -u +%Y%m%dT%H%M%SZ)"
sudo cp -a public "/srv/tiptap/releases/$RELEASE"
sudo chown -R root:root "/srv/tiptap/releases/$RELEASE"
sudo find "/srv/tiptap/releases/$RELEASE" -type d -exec chmod 755 {} +
sudo find "/srv/tiptap/releases/$RELEASE" -type f -exec chmod 644 {} +
sudo ln -sfn "/srv/tiptap/releases/$RELEASE" /srv/tiptap/next
sudo mv -Tf /srv/tiptap/next /srv/tiptap/current
curl --fail -I https://editor.example.com/app.js
printf 'Previous release: %s\n' "$PREVIOUS"
Сохраните напечатанный путь. Чтобы вернуть его, укажите именно каталог предыдущего релиза:
PREVIOUS=/srv/tiptap/releases/20261107T120000Z
sudo test -d "$PREVIOUS"
sudo ln -sfn "$PREVIOUS" /srv/tiptap/rollback
sudo mv -Tf /srv/tiptap/rollback /srv/tiptap/current
sudo journalctl -u caddy -n 50 --no-pager
Старые релизы автоматически не удаляются. Статическая смена каталога не требует перезапуска Caddy. При изменении Caddyfile сначала validate, затем reload. Для долгого кеширования используйте хешированные имена bundle; текущий простой пример требует обычной перезагрузки страницы после обновления. Черновики принадлежат origin: изменение схемы, домена или порта требует продуманной миграции данных.
Что взять в собственный проект
Начните с одного пользовательского действия: написать ответ, описать задачу или заполнить инструкцию. Выберите минимальную схему, подготовьте хороший исходный пример и определите, что именно сохраняется. Затем добавьте понятные состояния ошибок, восстановление после сбоя и серверные проверки. Лишь после этого расширяйте панель инструментов.
В архиве отдельно лежат конфигурации сценариев, общая настройка Editor, интерфейс и тесты. Это позволяет перенести нужную часть и заменить демоданные своими, сохранив проверяемую основу. Для библиотеки такого типа ценность создают не количество кнопок и не красивый toolbar, а то, насколько просто человеку получить нужный документ и не потерять свою работу.


