
Блог на 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+.jpgfallback - Браузер выбирает формат через
<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.