Блог и документация на Next.js с MDX

27 октября 2026
Автор

Антон Ларичев

Что такое MDX и зачем он нужен

MDX — это формат файлов, который объединяет Markdown и JSX. Он позволяет писать контент в привычном синтаксисе Markdown и при этом использовать React-компоненты прямо внутри текста.

Обычный Markdown ограничен: вы можете добавить текст, заголовки, списки и ссылки, но не можете встроить интерактивный компонент. MDX снимает это ограничение. Технический блог с примерами кода, документация с живыми демо, образовательные материалы с интерактивными упражнениями — всё это становится возможным без разделения контента и логики по разным системам.

Next.js App Router имеет встроенную поддержку MDX через пакет @next/mdx, что делает интеграцию минимальной по конфигурации.

Курсы PurpleSchool

Учитесь у практикующих разработчиков — без воды

40 000+ студентов · рейтинг 4.8 · гарантия возврата 30 дней

Установка и настройка

Для начала установите необходимые пакеты:

npm install @next/mdx @mdx-js/loader @mdx-js/react
npm install @types/mdx --save-dev

Затем обновите next.config.ts:

import type { NextConfig } from 'next';
import createMDX from '@next/mdx';

const nextConfig: NextConfig = {
  pageExtensions: ['js', 'jsx', 'ts', 'tsx', 'md', 'mdx'],
};

const withMDX = createMDX({
  options: {
    remarkPlugins: [],
    rehypePlugins: [],
  },
});

export default withMDX(nextConfig);

Добавьте файл mdx-components.tsx в корень проекта — он обязателен при использовании MDX с App Router:

import type { MDXComponents } from 'mdx/types';

export function useMDXComponents(components: MDXComponents): MDXComponents {
  return {
    ...components,
  };
}

Теперь файлы с расширением .mdx можно использовать как страницы Next.js.

Структура блога на MDX

Организация файлов

Для блога удобна следующая структура:

app/
  blog/
    page.tsx              # список статей
    [slug]/
      page.tsx            # страница статьи
content/
  blog/
    getting-started.mdx
    advanced-typescript.mdx
    nextjs-performance.mdx

Контент лежит отдельно от маршрутизации — это упрощает работу с файлами и позволяет переиспользовать статьи в разных частях сайта.

Frontmatter и gray-matter

Frontmatter — это блок метаданных в начале MDX-файла в формате YAML. Установите gray-matter для его парсинга:

npm install gray-matter

Пример MDX-файла content/blog/getting-started.mdx:

---
title: Начало работы с Next.js
date: 2026-08-15
description: Введение в разработку на Next.js App Router
author: Антон Ларичев
tags:
  - next-js
  - react
  - beginners
---

## Первые шаги

Next.js — это фреймворк для создания React-приложений...

Импортируйте и используйте компонент прямо в тексте:

<CodeSandbox url="https://example.com" />

Функция для получения всех статей с метаданными:

// lib/blog.ts
import fs from 'fs';
import path from 'path';
import matter from 'gray-matter';

const BLOG_DIR = path.join(process.cwd(), 'content/blog');

export interface PostMeta {
  slug: string;
  title: string;
  date: string;
  description: string;
  author: string;
  tags: string[];
}

export function getAllPosts(): PostMeta[] {
  const files = fs.readdirSync(BLOG_DIR);

  return files
    .filter((file) => file.endsWith('.mdx'))
    .map((file) => {
      const slug = file.replace('.mdx', '');
      const fullPath = path.join(BLOG_DIR, file);
      const fileContents = fs.readFileSync(fullPath, 'utf8');
      const { data } = matter(fileContents);

      return {
        slug,
        title: data.title,
        date: data.date,
        description: data.description,
        author: data.author,
        tags: data.tags ?? [],
      };
    })
    .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
}

export function getPostBySlug(slug: string) {
  const fullPath = path.join(BLOG_DIR, `${slug}.mdx`);
  const fileContents = fs.readFileSync(fullPath, 'utf8');
  return matter(fileContents);
}

Страница списка статей

// app/blog/page.tsx
import Link from 'next/link';
import { getAllPosts } from '@/lib/blog';

export const metadata = {
  title: 'Блог',
  description: 'Статьи о разработке на Next.js и React',
};

export default function BlogPage() {
  const posts = getAllPosts();

  return (
    <main className="max-w-3xl mx-auto py-12 px-4">
      <h1 className="text-4xl font-bold mb-8">Блог</h1>
      <ul className="space-y-8">
        {posts.map((post) => (
          <li key={post.slug}>
            <Link href={`/blog/${post.slug}`}>
              <h2 className="text-2xl font-semibold hover:text-blue-600">
                {post.title}
              </h2>
            </Link>
            <p className="text-gray-500 text-sm mt-1">
              {new Date(post.date).toLocaleDateString('ru-RU')} — {post.author}
            </p>
            <p className="text-gray-700 mt-2">{post.description}</p>
          </li>
        ))}
      </ul>
    </main>
  );
}

Динамическая страница статьи

// app/blog/[slug]/page.tsx
import { getAllPosts } from '@/lib/blog';
import path from 'path';
import { notFound } from 'next/navigation';
import fs from 'fs';

interface Props {
  params: Promise<{ slug: string }>;
}

export async function generateStaticParams() {
  const posts = getAllPosts();
  return posts.map((post) => ({ slug: post.slug }));
}

export async function generateMetadata({ params }: Props) {
  const { slug } = await params;
  const posts = getAllPosts();
  const post = posts.find((p) => p.slug === slug);

  if (!post) return {};

  return {
    title: post.title,
    description: post.description,
  };
}

export default async function BlogPostPage({ params }: Props) {
  const { slug } = await params;
  const filePath = path.join(process.cwd(), 'content/blog', `${slug}.mdx`);

  if (!fs.existsSync(filePath)) {
    notFound();
  }

  const { default: Post } = await import(`@/content/blog/${slug}.mdx`);

  return (
    <article className="max-w-3xl mx-auto py-12 px-4 prose prose-lg">
      <Post />
    </article>
  );
}

Кастомные компоненты в MDX

Одно из главных преимуществ MDX — возможность заменить стандартные HTML-элементы собственными компонентами через useMDXComponents.

Переопределение базовых элементов

// mdx-components.tsx
import type { MDXComponents } from 'mdx/types';
import Link from 'next/link';
import Image from 'next/image';

export function useMDXComponents(components: MDXComponents): MDXComponents {
  return {
    h2: ({ children }) => (
      <h2 className="text-3xl font-bold mt-10 mb-4 text-gray-900">{children}</h2>
    ),
    h3: ({ children }) => (
      <h3 className="text-2xl font-semibold mt-8 mb-3 text-gray-800">{children}</h3>
    ),
    a: ({ href, children }) => (
      <Link href={href ?? '#'} className="text-blue-600 hover:underline">
        {children}
      </Link>
    ),
    img: ({ src, alt }) => (
      <Image
        src={src ?? ''}
        alt={alt ?? ''}
        width={800}
        height={450}
        className="rounded-lg my-6"
      />
    ),
    blockquote: ({ children }) => (
      <blockquote className="border-l-4 border-blue-500 pl-4 italic text-gray-600 my-6">
        {children}
      </blockquote>
    ),
    ...components,
  };
}

Компонент Callout для документации

Для документации часто нужны блоки-выноски (tip, warning, danger):

// components/Callout.tsx
interface CalloutProps {
  type?: 'info' | 'warning' | 'danger' | 'tip';
  children: React.ReactNode;
}

const styles = {
  info: 'bg-blue-50 border-blue-400 text-blue-900',
  warning: 'bg-yellow-50 border-yellow-400 text-yellow-900',
  danger: 'bg-red-50 border-red-400 text-red-900',
  tip: 'bg-green-50 border-green-400 text-green-900',
};

const labels = {
  info: 'Информация',
  warning: 'Внимание',
  danger: 'Опасно',
  tip: 'Совет',
};

export function Callout({ type = 'info', children }: CalloutProps) {
  return (
    <div className={`border-l-4 p-4 rounded-r-lg my-6 ${styles[type]}`}>
      <p className="font-semibold mb-1">{labels[type]}</p>
      <div>{children}</div>
    </div>
  );
}

Использование в MDX-файле:

<Callout type="warning">
  Эта функция устарела в Next.js 15. Используйте вместо неё `generateMetadata`.
</Callout>

Подсветка синтаксиса

Для подсветки кода используйте rehype-pretty-code с темой на базе shiki:

npm install rehype-pretty-code shiki

Обновите конфигурацию:

// next.config.ts
import createMDX from '@next/mdx';
import rehypePrettyCode from 'rehype-pretty-code';
import type { Options } from 'rehype-pretty-code';

const prettyCodeOptions: Options = {
  theme: 'github-dark',
  keepBackground: true,
};

const withMDX = createMDX({
  options: {
    remarkPlugins: [],
    rehypePlugins: [[rehypePrettyCode, prettyCodeOptions]],
  },
});

Теперь блоки кода в MDX автоматически получают подсветку синтаксиса с поддержкой более 100 языков.

Структура документации

Для документации удобно использовать иерархическую структуру с навигацией:

content/
  docs/
    getting-started/
      installation.mdx
      quick-start.mdx
    guides/
      authentication.mdx
      deployment.mdx
    api/
      components.mdx
      hooks.mdx

Навигационная конфигурация

// lib/docs-nav.ts
export interface NavItem {
  title: string;
  href: string;
}

export interface NavSection {
  title: string;
  items: NavItem[];
}

export const docsNav: NavSection[] = [
  {
    title: 'Начало работы',
    items: [
      { title: 'Установка', href: '/docs/getting-started/installation' },
      { title: 'Быстрый старт', href: '/docs/getting-started/quick-start' },
    ],
  },
  {
    title: 'Руководства',
    items: [
      { title: 'Аутентификация', href: '/docs/guides/authentication' },
      { title: 'Деплой', href: '/docs/guides/deployment' },
    ],
  },
  {
    title: 'API',
    items: [
      { title: 'Компоненты', href: '/docs/api/components' },
      { title: 'Хуки', href: '/docs/api/hooks' },
    ],
  },
];

Layout для документации

// app/docs/layout.tsx
import { docsNav } from '@/lib/docs-nav';
import Link from 'next/link';

export default function DocsLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <div className="flex min-h-screen">
      <aside className="w-64 border-r px-6 py-8 shrink-0">
        <nav>
          {docsNav.map((section) => (
            <div key={section.title} className="mb-6">
              <h3 className="font-semibold text-sm text-gray-500 uppercase tracking-wider mb-2">
                {section.title}
              </h3>
              <ul className="space-y-1">
                {section.items.map((item) => (
                  <li key={item.href}>
                    <Link
                      href={item.href}
                      className="text-gray-700 hover:text-blue-600 text-sm block py-1"
                    >
                      {item.title}
                    </Link>
                  </li>
                ))}
              </ul>
            </div>
          ))}
        </nav>
      </aside>
      <main className="flex-1 px-8 py-8 max-w-4xl">{children}</main>
    </div>
  );
}

SEO и метаданные

Next.js позволяет генерировать метаданные на основе frontmatter статически через generateMetadata:

// app/blog/[slug]/page.tsx
export async function generateMetadata({ params }: Props) {
  const { slug } = await params;
  const posts = getAllPosts();
  const post = posts.find((p) => p.slug === slug);

  if (!post) return {};

  return {
    title: `${post.title} | Блог`,
    description: post.description,
    openGraph: {
      title: post.title,
      description: post.description,
      type: 'article',
      publishedTime: post.date,
      authors: [post.author],
      tags: post.tags,
    },
    twitter: {
      card: 'summary_large_image',
      title: post.title,
      description: post.description,
    },
  };
}

Генерация RSS-ленты

Для блога полезно добавить RSS-ленту через Route Handler:

// app/rss.xml/route.ts
import { getAllPosts } from '@/lib/blog';

export async function GET() {
  const posts = getAllPosts();
  const baseUrl = 'https://example.com';

  const items = posts
    .map(
      (post) => `
    <item>
      <title><![CDATA[${post.title}]]></title>
      <link>${baseUrl}/blog/${post.slug}</link>
      <description><![CDATA[${post.description}]]></description>
      <pubDate>${new Date(post.date).toUTCString()}</pubDate>
      <guid>${baseUrl}/blog/${post.slug}</guid>
    </item>`,
    )
    .join('');

  const xml = `<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>Блог</title>
    <link>${baseUrl}/blog</link>
    <description>Статьи о разработке</description>
    ${items}
  </channel>
</rss>`;

  return new Response(xml, {
    headers: { 'Content-Type': 'application/xml' },
  });
}

Оглавление статьи

Для длинных статей полезно автоматическое оглавление. Установите remark-toc:

npm install remark-toc

Добавьте плагин в конфигурацию:

import remarkToc from 'remark-toc';

const withMDX = createMDX({
  options: {
    remarkPlugins: [
      [remarkToc, { heading: 'Содержание', maxDepth: 3 }]
    ],
    rehypePlugins: [[rehypePrettyCode, prettyCodeOptions]],
  },
});

Теперь достаточно добавить ## Содержание в начало MDX-файла, и плагин автоматически сформирует список ссылок на заголовки.

Типичные проблемы

Импорты в MDX-файлах

MDX поддерживает импорт компонентов напрямую в файле:

import { Chart } from '@/components/Chart';

## Статистика за 2026 год

<Chart data={[10, 20, 30]} />

Но злоупотреблять этим не стоит — лучше зарегистрировать часто используемые компоненты глобально через useMDXComponents.

Проблема с динамическим импортом

При использовании import('@/content/blog/${slug}.mdx') TypeScript может не находить модуль. Добавьте объявление типа:

// types/mdx.d.ts
declare module '*.mdx' {
  import type { MDXComponents } from 'mdx/types';
  export const components: MDXComponents;
  export default function MDXContent(props: unknown): JSX.Element;
}

Сборка при большом количестве статей

Если статей несколько сотен, generateStaticParams может замедлить сборку. Используйте dynamicParams = true и ISR:

export const dynamicParams = true;
export const revalidate = 3600; // пересборка каждый час

Это позволит генерировать страницы по запросу и кэшировать их, не перестраивая весь сайт целиком при добавлении новой статьи.

Итог

MDX в связке с Next.js App Router даёт мощный инструмент для создания контентных сайтов. Ключевые моменты:

  • Установка через @next/mdx с минимальной конфигурацией
  • Метаданные через gray-matter frontmatter
  • Кастомные компоненты через useMDXComponents
  • Подсветка синтаксиса через rehype-pretty-code
  • Статическая генерация через generateStaticParams
  • SEO через generateMetadata на основе frontmatter

Такой подход используется в документации многих популярных библиотек, включая Next.js, Tailwind CSS и Radix UI.

Чтобы освоить Next.js глубоко — от маршрутизации App Router до деплоя — пройдите курс на PurpleSchool: Next.js — полный курс

Стрелочка влевоNext.js 15: новые возможности и breaking changesloading.js и error.js в Next.js App RouterСтрелочка вправо

Все гайды по Next-js

useSearchParams и usePathname в Next.js App RouterStreaming и Suspense в Next.jsServer Actions в Next.jsrevalidateTag и revalidatePath — инвалидация кэша в Next.jsPrisma ORM в Next.jsnotFound и redirect в серверных компонентах Next.jsNext.js с Supabase — авторизация и база данныхNext.js с GraphQL и Apollo ClientNext.js 15: новые возможности и breaking changesБлог и документация на Next.js с MDXloading.js и error.js в Next.js App RouterИнтернационализация i18n в Next.js App RouterАутентификация с JWT в App Router без сторонних библиотекgenerateStaticParams — статическая генерация динамических маршрутовNext.js cookies и headers в App RouterNext.js с Clerk: аутентификация пользователейNext.js after() — выполнение кода после отправки ответа
Открыть базу знаний

Отправить комментарий