Этот материал описывает архитектурные решения, лежащие в основе блога 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-запроса.
Выбранный стек:
output: 'export' и trailingSlash: true — статический экспорт без Node-рантайма.mdx файловnext build с понятным сообщениемprose-классы для типографики MDX-контента@next/mdx трансформирует .mdx-файлы в React-компоненты через webpack-лоадер во время next build. Это принципиально отличается от next-mdx-remote: нет runtime-зависимостей в клиентском бандле, нет гидратации MDX на клиенте — только статический HTML. Конфигурация в next.config.mjs включает плагины: remarkGfm для расширенного Markdown, rehypeSlug для якорей заголовков, rehypeAutolinkHeadings для ссылок к заголовкам, rehypePrettyCode для подсветки синтаксиса.
Маршрут 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`)Функция getAllArticles() читает директорию content/articles/ через fs.readdirSync, парсит frontmatter каждого .mdx-файла через gray-matter, и валидирует данные через Zod-схему. При ошибке валидации — throw new Error(...) с именем файла и списком нарушений. Это гарантирует, что невалидная статья сломает next build с понятным сообщением, а не молча попадёт на сайт.
Схема frontmatter (ArticleFrontmatterSchema):
title — строка, обязательнаdescription — строка, обязательнаdate — строка в формате YYYY-MM-DD (regex-валидация)slug — строка, обязательнаauthor — строка, обязательнаМетод .safeParse() используется вместо .parse() — это позволяет сформировать информативное сообщение с именем файла и списком всех нарушений, а не выбрасывать неинформативный ZodError.
Каждая страница статьи содержит два блока 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.
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/ включены
Статический 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-рантайма на продакшне.