MDX-безопасное написание
Версия: 1.1 Дата: 26.04.2026 Статус: Готов к обсуждению
Документ описывает технику безопасности при создании и правке любого MD-документа, индексируемого DocMap и рендерящегося через Docusaurus. Несоблюдение этих правил ломает сборку (npm run build) и блокирует деплой.
Изменения в v1.1 (26.04.2026): добавлен Класс 2 ловушек MDX — фигурные скобки парсятся как JSX-выражение и при отсутствии переменной вызывают ReferenceError во время server-side rendering. Описан реальный инцидент 26.04.2026 в Диагностика CMS — порядок проверок.md: конструкция backtick внутри backtick через экранирование (см. секцию «Класс 2» ниже) уронила сборку с ошибкой path is not defined. Добавлена обязательная третья проверка через grep по фигурным скобкам. Единственный надёжный способ показывать фигурные скобки — fenced code block с языковым тегом.
Корневая причина
Docusaurus использует MDX-парсер — расширение Markdown, в котором допустимы JSX-теги. MDX трактует любой < за которым сразу идёт цифра или буква как начало JSX-тега или компонента.
<2s → парсер ищет JSX-компонент <2s ... />
<500ms → то же
<TagName> → парсер ищет компонент TagName
Если такая последовательность встречается в обычном тексте (не внутри backtick-кода) — компиляция MDX падает с ошибкой Unexpected character или Unexpected token.
Это прямой источник поломки сборки даже если документ выглядит как обычный markdown.
Класс 2 — фигурные скобки как JSX-выражение
MDX-парсер интерпретирует любые фигурные скобки в тексте как JavaScript-выражение (JSX expression). Если внутри скобок имя без объявления — ReferenceError: <name> is not defined при server-side rendering, сборка Docusaurus падает с Can't render static file.
Этот класс опаснее Класса 1 — обычный grep <digit его не ловит, а ошибка проявляется только при npm run build, не на этапе записи документа.
Опасные паттерны Класса 2
В обычном тексте (вне fenced code block):
- Локализационные ключи с шаблонными подстановками (например,
Path '%{path}' already existsс открывающей фигурной скобкой) — MDX парсит подстановку как JSX-выражение, переменной в области видимости нет, ошибка. - JavaScript-шаблонные строки
${variable}— то же самое. - Объекты в YAML или JSON в plain-тексте (
{ widget: string, label: Path }) — открывающая скобка запускает JSX-парсинг. - Двойные фигурные
{{ slug }}— парсятся как JSX-фрагмент.
В одиночном backtick-инлайне защита ненадёжна — в зависимости от версии Docusaurus и MDX-парсера фигурные скобки внутри backtick могут парситься как JSX. Не полагаться.
Антипаттерн (реальный инцидент 26.04.2026)
В документе cms-system/reference/Диагностика CMS — порядок проверок.md была попытка показать цитату из исходников Decap CMS через вложенный inline-code: внешний backtick-инлайн, а внутри — экранированный backtick через обратный слэш. Сама проблемная конструкция (показана здесь в fenced code block для безопасности):
`pathExists: \`Path '%{path}' already exists\``
MDX не понимает такое экранирование. Парсер закрывает внешний backtick после первого экранированного backtick, потом начинает читать оставшееся как обычный текст. В этом «обычном тексте» оказывается шаблонная подстановка с открывающей фигурной скобкой, MDX видит её и парсит как JSX-выражение, ищет переменную path в области видимости компонента — её нет.
Результат при npm run build:
Error: Can't render static file for pathname "/cms-system/reference/Диагностика CMS — порядок проверок"
[cause]: ReferenceError: path is not defined
Исправление — вынос в отдельный fenced code block с языковым тегом:
pathExists: "Path '%{path}' already exists"
Внутри fenced code MDX не парсит JSX вообще — ни тегов, ни выражений. Безопасно для любого содержимого, включая фигурные скобки, шаблонные подстановки, JS-объекты.
Безопасные формы для Класса 2
Единственный надёжный способ — fenced code block с указанием языка:
```js
pathExists: "Path '%{path}' already exists"
```
```yaml
meta: { path: { widget: string, label: Path } }
```
```bash
curl -X POST "$URL" -d '{"action":"info"}'
```
Внутри fenced code block MDX не парсит JSX вообще — ни тегов, ни выражений. Это работает на всех версиях Docusaurus и MDX 2/3.
HTML-entity для редких случаев в plain-тексте:
{— открывающая фигурная скобка;}— закрывающая фигурная скобка.
Это рендерится визуально как фигурные скобки, но парсер MDX entity не считает за начало JSX-выражения.
Что НЕ работает для Класса 2
- ❌ Экранирование через обратный слэш —
\{\}не работает в MDX 2/3. - ❌ Двойные фигурные
{{ ... }}— это JSX-фрагмент, тоже парсится. - ❌ Одиночный backtick-инлайн с фигурными — ненадёжен, не гарантирует.
- ❌ Backtick внутри backtick через ``` — этот приём ломает inline code и провоцирует JSX-парсинг.
Правило
При любом упоминании фигурных скобок в документации DocMap — выносить в fenced code block с языковым тегом.
Это касается:
- цитат из исходников языков программирования (JS, TS, Python — везде есть фигурные скобки);
- примеров YAML/JSON-конфигов (содержат фигурные скобки для объектов);
- упоминаний шаблонных строк (
${variable},{{ slug }}); - примеров локализационных ключей с подстановками;
- любых JS-выражений в обычном тексте.
Опасные конструкции
Запрещено в plain-тексте документа:
<2s— парсер ищет JSX-компонент<2s>.<500ms— то же.<1.5s,<15 min,<10%— то же.<TagName>— если не намеренный JSX-компонент Docusaurus, парсер пытается срендерить как компонент.>2 сервиса— обычно работает, но небезопасно при некоторых конфигурациях парсера, лучше избегать.
Безопасные формы
Вариант 1. HTML-entity (предпочтительный)
<2s вместо <2s
<500ms вместо <500ms
>2 сервиса вместо >2 сервиса
Визуально в браузере выглядит идентично исходному <2s или >2 сервиса. Парсер MDX не путает с JSX-тегом.
Вариант 2. Пробел после <
< 2s
< 500ms
Подходит, если стилистически уместно. Парсер MDX не считает < за стартом JSX-тега, если после идёт пробел.
Вариант 3. Backtick-инлайн
`<2s`
`<TagName>`
Внутри backtick-кода MDX не парсит JSX. Безопасно для технических примеров.
Вариант 4. Fenced code block
```
<2s — это безопасно внутри блока кода
<TagName>
```
Внутри ``` ``` MDX не парсит JSX. Безопасно для блоков примеров.
Обязательная проверка после записи документа
После каждого docs_create_file или docs_patch_section — самопроверка через grep, три команды:
# Класс 1: < или > перед цифрой
grep -nE '<[0-9]' путь/к/файлу.md
grep -nE '>[0-9]' путь/к/файлу.md
# Класс 2: открывающая фигурная скобка
grep -nE '\{' путь/к/файлу.md
Каждое совпадение проверяется визуально:
- Внутри fenced code block
```(тройной backtick) — безопасно, оставляем. - Внутри одиночного backtick-инлайна — для Класса 1 безопасно, для Класса 2 ненадёжно. Для фигурных скобок переоформить в fenced.
- В plain-тексте — обязательно переоформить.
Автоисправление для Класса 1 (массовая замена):
sed -i 's/<\([0-9]\)/\<\1/g' путь/к/файлу.md
Эта команда заменяет каждый < за которым идёт цифра — на HTML-entity и эту цифру. Обратный слэш перед & обязателен в shell, чтобы избежать раскрытия истории.
Автоисправление для Класса 2 невозможно — каждый случай требует ручной переработки в fenced code block с правильным языковым тегом (js, yaml, bash, json и так далее).
Случай из практики
24-25 апреля 2026 — после массовой записи 50+ архитектурных документов в vitiana-api-platform/ сборка Docusaurus упала с десятками ошибок MDX. Причина — везде <2s, <500ms, <1.5s в обычном тексте.
Решение — массовая замена через sed по всем затронутым файлам:
find docs/vitiana-api-platform -name '*.md' -exec sed -i 's/<\([0-9]\)/\<\1/g' {} \;
После замены — npm run build прошёл без ошибок MDX. Документы визуально не изменились (<2s и <2s рендерятся одинаково).
Урок: проверять MDX-safe до записи, а не после падения сборки. Чек-лист — в Правила оформления документов.
Тонкости
Что НЕ ломается
<без цифры/буквы сразу после:a < b(с пробелами) — безопасно.<в URL:https://example.com/path?a=<value>— обычно безопасно, но лучше экранировать или поместить в backtick.- HTML-entity
<>— всегда безопасно. - Внутри
`code`или``` block ```— всегда безопасно.
Что ломается реже, но возможно
>за которым цифра:>2 сервиса— на некоторых версиях MDX компилируется, на других нет. Лучше>2 сервисадля надёжности.- Текст похожий на JSX-атрибут:
<div className="foo">в plain-тексте — парсится как открытие компонентаdiv, что иногда проходит, иногда нет. Лучше backtick.
Точное правило MDX
Парсер MDX считает < началом JSX-тега, если сразу после него идёт:
- буква (a-z, A-Z) — открытие именованного компонента;
>— фрагмент<>;/— закрытие тега</…>.
Цифра формально не должна включать JSX-парсинг по спецификации MDX 2/3, но на практике в Docusaurus с интеграцией ремарка/рехайпа конструкция <2s вызывает синтаксическую ошибку. Поэтому правило — экранировать <digit в любом случае.
Связанная документация
- Правила оформления документов — общие правила, чек-лист создания и правки.
- Чек-лист — создавать так чтобы сразу работало — обязательные проверки до и после записи.
- Стандарт работы с системой документации — единый свод правил организации.