Когда админка становится обузой
Помните эту историю? Запускаете блог, портфолио или документацию. Первая мысль: «Нужна CMS». Дальше — выбор между Contentful, Strapi, Sanity... Настраиваете схемы, прикручиваете API, платите за хостинг или SaaS-план.
Через месяц понимаете: админка используется раз в неделю, контент пишете вы (технический человек), а половина времени уходит на борьбу с ограничениями конструктора полей.
Если контент создают разработчики, а не редакторы — headless CMS может быть избыточен. MDX даёт контроль без лишних слоёв.
Что такое MDX и почему он здесь
MDX = Markdown + JSX. Пишете обычный маркдаун, но можете вставлять React-компоненты прямо в текст:
## Заголовок статьи
Обычный текст параграфа.
<MetricsGrid
metrics={[
{ value: "×3", label: "production" },
{ value: "99.9%", label: "uptime" },
]}
/>
Продолжение текста...Компоненты рендерятся в HTML на этапе сборки (если SSG) или на сервере (SSR). Никакого рантайма, никаких внешних API.
Когда MDX лучше headless CMS
1. Технический контент
Документация, блоги разработчиков, портфолио:
- Контент в Git → version control, code review, blame
- Markdown — родной формат для техписов
- Вставки кода с подсветкой синтаксиса из коробки
- Нет задержек API, нет rate limits
2. Небольшие объёмы контента
Если у вас десятки статей, а не тысячи продуктов — MDX справится без индексов и поиска. Меньше 100 файлов? Сборка мгновенная.
3. Нужны кастомные компоненты
CMS даёт «rich text editor». MDX даёт весь арсенал React:
<Callout type="warning">Важное предупреждение с иконкой и цветом</Callout>
<BeforeAfter before={[...]} after={[...]} />
<TechStack stack={["React", "TypeScript"]} />
<Video src="https://..." title="Демо" />Любой компонент из вашей дизайн-системы — сразу в контент. Никаких «custom blocks» через JSON-схемы.
4. Нужен type-safety
MDX + TypeScript = type-safe контент. Фронтматтер проверяется на этапе сборки:
// src/shared/lib/mdx/posts.ts
type PostFrontmatter = {
title: string;
slug: string;
date: string;
summary: string;
tags: string[];
featured?: boolean;
draft?: boolean;
};
// Парсинг с валидацией через Zod или подобное
export async function getPostBySlug(slug: string) {
const source = await readFile(`content/posts/${slug}.mdx`);
const { data, content } = matter(source);
// Type-safe frontmatter
const frontmatter = PostFrontmatterSchema.parse(data);
return { frontmatter, content };
}Ошибка в date формате или отсутствие обязательного поля — сборка упадёт. В CMS — узнаете в рантайме.
Кастомные компоненты для MDX
Создание компонента
Компоненты для MDX — обычные React-компоненты:
// src/shared/ui/mdx-components.tsx
export function Callout({
type = "info",
children
}: {
type?: "info" | "warning" | "success" | "error";
children: React.ReactNode;
}) {
return (
<div className={cn(
"rounded-[var(--radius)] border p-4",
type === "warning" && "border-yellow-500 bg-yellow-50",
type === "error" && "border-red-500 bg-red-50",
// ...
)}>
{children}
</div>
);
}Регистрация компонентов
// src/shared/ui/mdx-components.tsx
export const mdxComponents = {
Callout,
MetricsGrid,
TechStack,
BeforeAfter,
Quote,
Video,
Gallery,
// Переопределяем стандартные элементы
h1: (props) => <h1 className="text-4xl font-bold" {...props} />,
a: (props) => <a className="text-primary hover:underline" {...props} />,
};Использование в MDX
После регистрации компоненты доступны без импортов:
---
title: "Статья"
---
## Секция
<Callout type="warning">Автоматически доступно!</Callout>Next.js + @next/mdx делает это из коробки через mdx-components.tsx в корне проекта.
Type-safe работа с frontmatter
Схема валидации (Zod)
// src/shared/lib/mdx/schema.ts
import { z } from "zod";
export const PostFrontmatterSchema = z.object({
title: z.string().min(1),
slug: z.string().regex(/^[a-z0-9-]+$/),
date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
summary: z.string().min(10).max(300),
tags: z.array(z.string()).min(1),
featured: z.boolean().optional(),
draft: z.boolean().optional(),
readTime: z.string().optional(),
author: z.string().optional(),
});
export type PostFrontmatter = z.infer<typeof PostFrontmatterSchema>;Парсинг с валидацией
// src/shared/lib/mdx/posts.ts
import matter from "gray-matter";
import { PostFrontmatterSchema } from "./schema";
export async function getAllPosts() {
const files = await readdir("content/posts");
const posts = await Promise.all(
files
.filter((f) => f.endsWith(".mdx") && !f.endsWith(".en.mdx"))
.map(async (file) => {
const source = await readFile(`content/posts/${file}`);
const { data } = matter(source);
// Валидация на этапе сборки
const frontmatter = PostFrontmatterSchema.parse(data);
// Фильтруем черновики в проде
if (process.env.NODE_ENV === "production" && frontmatter.draft) {
return null;
}
return frontmatter;
})
);
return posts.filter(Boolean).sort((a, b) => b.date.localeCompare(a.date));
}Ошибка в frontmatter → сборка упадёт → вы узнаете до деплоя. В CMS — узнаете когда пользователь увидит 500.
Типизация в компонентах
// app/blog/page.tsx
import { getAllPosts } from "@/shared/lib/mdx/posts";
export default async function BlogPage() {
const posts = await getAllPosts();
return (
<div>
{posts.map((post) => (
// post.title, post.slug — все типизированы
<PostCard key={post.slug} {...post} />
))}
</div>
);
}Content layer архитектура
Структура проекта
content/
posts/
*.mdx # Русские версии
*.en.mdx # Английские версии
projects/
*.mdx
pages/
*.mdx
src/
shared/
lib/
mdx/
posts.ts # Утилиты для постов
projects.ts # Утилиты для проектов
schema.ts # Zod-схемы
ui/
mdx-components.tsx # MDX компоненты
app/
blog/
page.tsx # Список постов
[slug]/page.tsx # Страница поста (SSG)
Content utilities layer
Все операции с контентом — через утилиты:
// src/shared/lib/mdx/posts.ts
export async function getAllPosts(): Promise<PostFrontmatter[]>;
export async function getFeaturedPosts(): Promise<PostFrontmatter[]>;
export async function getPostBySlug(slug: string): Promise<Post>;
export async function getAllPostSlugs(): Promise<string[]>;Изоляция от Next.js:
- Утилиты не знают про
app/директорию - Можно переиспользовать в API routes, SSR, SSG
- Легко тестировать
SSG для постов
// app/blog/[slug]/page.tsx
import { getAllPostSlugs, getPostBySlug } from "@/shared/lib/mdx/posts";
import { compileMDX } from "next-mdx-remote/rsc";
import { mdxComponents } from "@/shared/ui/mdx-components";
// Генерация статических путей
export async function generateStaticParams() {
const slugs = await getAllPostSlugs();
return slugs.map((slug) => ({ slug }));
}
// Генерация metadata
export async function generateMetadata({ params }) {
const post = await getPostBySlug(params.slug);
return {
title: post.frontmatter.title,
description: post.frontmatter.summary,
};
}
// Рендер страницы
export default async function PostPage({ params }) {
const { frontmatter, content } = await getPostBySlug(params.slug);
const { content: mdxContent } = await compileMDX({
source: content,
components: mdxComponents,
});
return (
<article>
<h1>{frontmatter.title}</h1>
<div className="prose">{mdxContent}</div>
</article>
);
}Результат: все посты собираются в HTML на этапе билда. Нулевое время загрузки контента, отличный SEO.
Сравнение: MDX vs headless CMS
Когда всё-таки нужна CMS
- Нетехнические редакторы (маркетинг, копирайтеры)
- Тысячи единиц контента
- Нужен поиск, фильтрация, теги
- Контент обновляется несколько раз в день
- Требуется workflow: draft → review → publish
- Мультиязычность с переводами от разных людей
Практический пример: этот сайт
Этот сайт (potapov.me) использует MDX для проектов и постов:
Стек контента:
- MDX файлы в
content/posts/,content/projects/ - Frontmatter валидация через Zod
- Кастомные компоненты:
Callout,MetricsGrid,BeforeAfter,TechStack - SSG через Next.js App Router
- i18n:
*.mdx(русский) +*.en.mdx(английский)
Workflow:
- Пишу статью в VSCode (подсветка, превью)
git commit→ автоматически валидация frontmattergit push→ CI собирает статику- Деплой через PM2 reload (zero-downtime)
От идеи до публикации — 10 минут. Без логинов в админки, без API токенов, без месячных счетов.
Как начать с MDX
1. Установка (Next.js)
npm install @next/mdx @mdx-js/loader @mdx-js/react gray-matter
npm install -D @types/mdx2. Конфигурация Next.js
// next.config.ts
import createMDX from "@next/mdx";
const withMDX = createMDX({
extension: /\.mdx?$/,
options: {
remarkPlugins: [],
rehypePlugins: [],
},
});
export default withMDX({
pageExtensions: ["ts", "tsx", "md", "mdx"],
});3. Создайте первый MDX файл
---
title: "Первый пост"
slug: "first-post"
date: "2025-11-14"
summary: "Тестируем MDX"
tags: ["test"]
---
## Заголовок
Обычный текст.
<Callout type="info">Кастомный компонент!</Callout>4. Утилита для чтения
// src/shared/lib/mdx/posts.ts
import fs from "fs/promises";
import path from "path";
import matter from "gray-matter";
const POSTS_DIR = path.join(process.cwd(), "content/posts");
export async function getAllPosts() {
const files = await fs.readdir(POSTS_DIR);
const posts = await Promise.all(
files
.filter((f) => f.endsWith(".mdx"))
.map(async (file) => {
const content = await fs.readFile(path.join(POSTS_DIR, file), "utf-8");
const { data } = matter(content);
return data;
})
);
return posts;
}5. Рендер в Next.js
// app/blog/[slug]/page.tsx
import { compileMDX } from "next-mdx-remote/rsc";
export default async function Post({ params }) {
const source = await readPostFile(params.slug);
const { content } = await compileMDX({ source });
return <article>{content}</article>;
}Грабли и решения
Граблі #1: Медленная сборка
Проблема: 100+ MDX файлов → сборка 10+ секунд
Решение:
- Кешируйте парсинг (Next.js делает автоматически)
- Используйте
next-mdx-remote/rscвместо@next/mdxдля больших объёмов - Выносите тяжелые rehype/remark плагины только для необходимых файлов
Граблі #2: Нет live preview
Проблема: Нужен npm run dev + F5 для просмотра изменений
Решение:
- VSCode расширение MDX Preview
- Или используйте CMS UI поверх MDX (Tina CMS, Keystatic)
Граблі #3: Поиск по контенту
Проблема: MDX — статические файлы, нет полнотекстового поиска
Решение:
- Индексируйте на этапе сборки в JSON → клиентский поиск (Fuse.js)
- Algolia DocSearch (бесплатно для опенсорса)
- Lunr.js для статических индексов
Итог
MDX — не замена CMS для всех случаев. Но для технических проектов с контролем над кодом он даёт:
✅ Type-safety — ошибки на этапе сборки, не в продакшене ✅ Git workflow — version control, code review, rollback ✅ Кастомные компоненты — весь React без ограничений ✅ Нулевая задержка — SSG, никаких API запросов ✅ Нулевая стоимость — нет SaaS подписок
Если контент пишут разработчики, а не редакторы — MDX может быть проще, быстрее и дешевле headless CMS. Проверьте, может быть ваш случай.
См. также:
- Feature-Sliced Design — как структурировать MDX утилиты по слоям
- Slot-Me — платформа бронирования — проект с MDX-контентом в проде

