Как работает MDX-движок документации b1m-pulse.ru


title: "Как работает MDX-движок документации b1m-pulse.ru" description: "Технический обзор архитектуры MDX-блога: от .dyn файла до статической HTML-страницы через Next.js App Router и @next/mdx" date: "2026-06-01" slug: "placeholder-mdx-engine" author: "b1m-pulse.ru"

Введение

Этот материал описывает архитектурные решения, лежащие в основе блога b1m-pulse.ru — статического MDX-движка, построенного на Next.js 16 App Router с output: 'export'. Цель: объяснить, как Dynamo-скрипты (*.dyn) превращаются в читаемую HTML-документацию, а статьи блога — в SEO-оптимизированные статические страницы с JSON-LD разметкой для AI-краулеров.

Стек и мотивация

Ключевая проблема, которую решает этот стек: AI-краулеры (GPTBot, ClaudeBot, PerplexityBot, Google-Extended, YandexGPT) не исполняют JavaScript. Старый Vite SPA отдавал около 104 слова сырого HTML при обходе краулером — против ~5000 слов на SSR-сайтах конкурентов. Решение: переход на Next.js SSG, где весь HTML генерируется в момент сборки и отдаётся краулеру без единого JS-запроса.

Выбранный стек:

Как работает @next/mdx

@next/mdx трансформирует .mdx-файлы в React-компоненты через webpack-лоадер во время next build. Это принципиально отличается от next-mdx-remote: нет runtime-зависимостей в клиентском бандле, нет гидратации MDX на клиенте — только статический HTML. Конфигурация в next.config.mjs включает плагины: remarkGfm для расширенного Markdown, rehypeSlug для якорей заголовков, rehypeAutolinkHeadings для ссылок к заголовкам, rehypePrettyCode для подсветки синтаксиса.

Маршруты блога и generateStaticParams

Маршрут app/blog/[slug]/page.tsx использует generateStaticParams для генерации статических страниц. При output: 'export' Next.js не рендерит страницы вне generateStaticParams — поэтому флаг dynamicParams = false не нужен (и опасен: ломает next dev из-за бага #56253).

export async function generateStaticParams() {
  const articles = getAllArticles()
  return articles.map((a) => ({ slug: a.slug }))
}

Для динамического импорта MDX-файлов используется относительный путь, а не alias @/* — потому что tsconfig paths указывает @/* → ./src/*, и @/content не резолвится:

const mod = await import(`../../content/articles/${slug}.mdx`)

lib/articles.ts и Zod-валидация

Функция getAllArticles() читает директорию content/articles/ через fs.readdirSync, парсит frontmatter каждого .mdx-файла через gray-matter, и валидирует данные через Zod-схему. При ошибке валидации — throw new Error(...) с именем файла и списком нарушений. Это гарантирует, что невалидная статья сломает next build с понятным сообщением, а не молча попадёт на сайт.

Схема frontmatter (ArticleFrontmatterSchema):

Метод .safeParse() используется вместо .parse() — это позволяет сформировать информативное сообщение с именем файла и списком всех нарушений, а не выбрасывать неинформативный ZodError.

JSON-LD для AI-видимости

Каждая страница статьи содержит два блока JSON-LD разметки:

BlogPosting — описывает статью для поисковых роботов:

{
  "@context": "https://schema.org",
  "@type": "BlogPosting",
  "headline": "Заголовок статьи",
  "datePublished": "2026-06-01",
  "author": { "@type": "Person", "name": "b1m-pulse.ru" },
  "publisher": { "@type": "Organization", "name": "b1m-pulse.ru" }
}

BreadcrumbList — помогает поисковику понять иерархию страниц: Главная → Блог → Статья.

Оба блока вставляются через dangerouslySetInnerHTML с предварительной XSS-обработкой через safeJsonLd() — функцию, которая заменяет <, >, & на Unicode-escape последовательности (<, >, &). Это предотвращает XSS-атаку через </script> в данных frontmatter.

Путь данных: от .mdx до HTML

content/articles/slug.mdx
  ↓ fs.readdirSync (build time)
lib/articles.ts — getAllArticles() + Zod validate
  ↓ generateStaticParams
app/blog/[slug]/page.tsx
  ↓ await import('../../content/articles/${slug}.mdx')
  ↓ JSON-LD BlogPosting + BreadcrumbList
out/blog/slug/index.html — статический HTML
  ↓ next-sitemap postbuild
out/sitemap-0.xml — /blog/ и /blog/slug/ включены

Деплой и nginx

Статический out/ деплоится на nginx на reg.ru VPS (95.163.226.47), домен b1m-pulse.ru. Nginx раздаёт файлы напрямую — без Node-процесса. API-запросы проксируются на FastAPI backend через location /api/. Это обеспечивает максимальную производительность раздачи контента и минимальную стоимость сервера.

Итог

MDX-движок b1m-pulse.ru — полностью статическое решение: MDX компилируется в HTML при сборке, Zod валидирует контракт frontmatter на этапе CI, JSON-LD разметка обеспечивает видимость для AI-краулеров. Результат: правильный HTML с полным контентом при первом обращении краулера, без JavaScript, без Node-рантайма на продакшне.