разработка

Блог на Markdown в Next.js

Дата публикации

ℹ️

ℹ️ Что covered в этом посте

Как мы создали файловый блог с использованием MDX и Next.js 15 — без базы данных, без CMS, только файлы markdown со встроенными компонентами React.

Создание MDX-блога в Next.js

После экспериментов с различными решениями CMS мы вернулись к одному из самых простых и мощных подходов: система блога на основе MDX, работающая на Next.js. MDX даёт нам переносимость Markdown в сочетании с выразительностью React — лучшее из обоих миров.

Markdown против MDX

Markdown отличен для контента. MDX делает шаг дальше, позволяя использовать компоненты React напрямую внутри ваших постов — без плагинов, без шорткодов, без этапа пост-обработки.

ℹ️

📄 Обычный Markdown

  • Текст, заголовки, списки, блоки кода
  • Ссылки и изображения
  • Таблицы (через GFM)
  • Только статический контент
ℹ️

✨ MDX

  • Всё, что может Markdown
  • Пользовательские компоненты React inline
  • Props, layouts, интерактивность
  • Богатый структурированный контент

Архитектура

Каждый пост находится в собственной папке под storage/posts/, названной с датой публикации и slug. Система читает их во время сборки — без запросов к базе данных, без вызовов API во время выполнения.

storage/posts/
├── 2026-02-02-markdown-blog-v-next-js/
│   ├── post.mdx          ← контент + frontmatter
│   ├── cover.jpg         ← фон hero
│   ├── cover.webp        ← hero (webp)
│   ├── thumb.jpg         ← превью карточки (2× retina)
│   └── thumb.webp        ← превью карточки (webp)
└── ...

Frontmatter с gray-matter

Мы используем gray-matter для разбора YAML-frontmatter для метаданных поста:

import matter from 'gray-matter'

const { data, content } = matter(fileContent)

const meta = {
  title: data.title,
  description: data.description,
  heroImage: data.heroImage,
  thumbImage: data.thumbImage,
}
⚠️

⚠️ Пути к изображениям

Изображения подаются через маршрут API по адресу /api/posts/images/[...path], а не из каталога public/. Если вы опустите heroImage или thumbImage в frontmatter, система по умолчанию использует /api/posts/images/{folder}/cover.jpg и thumb.jpg соответственно.

Компоненты MDX

Это то, где MDX действительно превосходит обычный Markdown. Мы регистрируем пользовательские компоненты глобально — они доступны в каждом посте без необходимости импорта внутри файла MDX.

✅ Доступные компоненты

Макет

  • Callout — info, warning, danger, success, dark варианты
  • Grid — адаптивные макеты на 2 или 4 колонки

Контент

  • TimelineItem — временные записи с датами
  • LessonGroup — именованные маркированные списки
  • ExploitChain / ExploitStep — numbered потоки шагов

Использование компонентов на практике

Вот как выглядит Grid из Callouts в исходном MDX:

<Grid cols={2}>
  <Callout variant="info" title="Первый">
    Контент здесь...
  </Callout>
  <Callout variant="success" title="Второй">
    Ещё контент...
  </Callout>
</Grid>

И временная шкала:

<TimelineItem date="1 января" color="blue">
  Запуск проекта — выбор стека технологий
</TimelineItem>
<TimelineItem date="15 января" color="purple">
  Первый опубликованный пост
</TimelineItem>

Рендеринг с next-mdx-remote

next-mdx-remote берёт на себя тяжёлую работу на стороне рендеринга:

  • Полная поддержка App Router с серверными компонентами
  • Регистрация пользовательских компонентов через проп components
  • Подсветка синтаксиса через rehype-highlight
  • GitHub Flavored Markdown через remarkGfm
  • Автоматически генерируемые якоря заголовков через rehypeSlug

Обработка удалённых постов

Посты могут быть "удалены" без удаления папки — усечь файл до 0 байт:

const stats = await fs.stat(postPath)
if (stats.size === 0) {
  // Вернуть 410 Gone — SEO-дружественный способ выхода контента из употребления
}

Папка, изображения и URL остаются нетронутыми. Сервер возвращает правильный HTTP-статус 410 вместо 404.

Производительность

ℹ️

⚡ Время сборки

  • Без базы данных — чистое чтение файловой системы
  • Статически генерируется во время сборки
  • Дружественный к CDN вывод
  • Недействительность кэша на основе mtime файлов
ℹ️

🖼️ Изображения

  • Двойной формат: .webp + .jpg fallback
  • Браузер выбирает формат через <picture> — без JS
  • Превью в 2× для retina-дисплеев
  • Длинные заголовки кэша на всех активах

Написание нового поста

🚀 Рабочий процесс

Шаги

  • Создайте папку: storage/posts/YYYY-MM-DD-vash-slug/
  • Добавьте post.mdx с title, description, category и tags
  • Поместите изображения cover и thumb (пары jpg + webp)
  • Используйте любой компонент MDX — импорты не нужны
  • Commit и push — готово

Без логина, без админ-интерфейса, без миграций базы данных. Просто файлы в git.

#nextjs#mdx#markdown#blog#typescript#react