Markdown и MDX

Markdown — это облегчённый язык разметки для форматирования текста. Он позволяет писать с использованием простого текстового синтаксиса и преобразовывать его в структурно валидный HTML. Часто используется для написания контента на сайтах и в блогах.

Пример записи:

I **love** using [Next.js](https://nextjs.org/)

Результат:

<p>I <strong>love</strong> using <a href="https://nextjs.org/">Next.js</a></p>

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

Next.js поддерживает как локальный MDX-контент внутри приложения, так и удалённые MDX-файлы, загружаемые динамически на сервере. Плагин Next.js преобразует Markdown и React-компоненты в HTML, включая поддержку использования в серверных компонентах (по умолчанию в App Router).

@next/mdx

Пакет @next/mdx используется для настройки Next.js для обработки Markdown и MDX. Он работает с локальными файлами, позволяя создавать страницы с расширением .mdx прямо в директориях /pages или /app.

Давайте разберём, как настроить и использовать MDX в Next.js.

Начало работы

Установите необходимые пакеты для рендеринга MDX:

Терминал
npm install @next/mdx @mdx-js/loader @mdx-js/react @types/mdx

Создайте файл mdx-components.tsx в корне вашего приложения (src/ или родительской папке app/):

Важно: mdx-components.tsx обязателен для использования MDX с App Router и не будет работать без него.

import type { MDXComponents } from 'mdx/types'

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

Обновите файл next.config.js в корне проекта для настройки работы с MDX:

next.config.js
const withMDX = require('@next/mdx')()

/** @type {import('next').NextConfig} */
const nextConfig = {
  // Настройка `pageExtensions` для включения MDX-файлов
  pageExtensions: ['js', 'jsx', 'mdx', 'ts', 'tsx'],
  // Опционально: добавьте другие настройки Next.js
}

module.exports = withMDX(nextConfig)

Затем создайте новую MDX-страницу в директории /app:

  your-project
  ├── app
  │   └── my-mdx-page
  │       └── page.mdx
  └── package.json

Теперь вы можете использовать Markdown и импортировать React-компоненты прямо в MDX-странице:

import { MyComponent } from 'my-components'

# Добро пожаловать на мою MDX-страницу!

Это **жирный** и _курсивный_ текст.

Пример списка в Markdown:

- Один
- Два
- Три

Посмотрите мой React-компонент:

<MyComponent />

При переходе по маршруту /my-mdx-page должна отображаться ваша MDX-страница.

Удалённый MDX

Если ваши файлы Markdown или MDX находятся в другом месте, вы можете загружать их динамически на сервере. Это полезно для контента, хранящегося в отдельной локальной папке, CMS, базе данных или где-либо ещё. Популярный пакет для этого — next-mdx-remote.

Важно: Будьте осторожны. MDX компилируется в JavaScript и выполняется на сервере. Загружайте MDX-контент только из доверенных источников, иначе это может привести к удалённому выполнению кода (RCE).

Следующий пример использует next-mdx-remote:

import { MDXRemote } from 'next-mdx-remote/rsc'

export default async function RemoteMdxPage() {
  // Текст MDX — может быть из локального файла, базы данных, CMS, fetch и т.д.
  const res = await fetch('https://...')
  const markdown = await res.text()
  return <MDXRemote source={markdown} />
}
import { MDXRemote } from 'next-mdx-remote/rsc'

export default async function RemoteMdxPage() {
  // Текст MDX — может быть из локального файла, базы данных, CMS, fetch и т.д.
  const res = await fetch('https://...')
  const markdown = await res.text()
  return <MDXRemote source={markdown} />
}

При переходе по маршруту /my-mdx-page-remote должна отображаться ваша MDX-страница.

Макеты

Для общего макета MDX-страниц вы можете использовать встроенную поддержку макетов в App Router.

export default function MdxLayout({ children }: { children: React.ReactNode }) {
  // Создайте общий макет или стили здесь
  return <div style={{ color: 'blue' }}>{children}</div>
}
export default function MdxLayout({ children }) {
  // Создайте общий макет или стили здесь
  return <div style={{ color: 'blue' }}>{children}</div>
}

Плагины Remark и Rehype

Вы можете опционально использовать плагины remark и rehype для преобразования MDX-контента.

Например, можно использовать remark-gfm для поддержки GitHub Flavored Markdown.

Поскольку экосистема remark и rehype работает только с ESM, вам нужно использовать файл конфигурации next.config.mjs.

next.config.mjs
import remarkGfm from 'remark-gfm'
import createMDX from '@next/mdx'

/** @type {import('next').NextConfig} */
const nextConfig = {
  // Настройка `pageExtensions` для включения MDX-файлов
  pageExtensions: ['js', 'jsx', 'mdx', 'ts', 'tsx'],
  // Опционально: добавьте другие настройки Next.js
}

const withMDX = createMDX({
  // Добавьте плагины Markdown по желанию
  options: {
    remarkPlugins: [remarkGfm],
    rehypePlugins: [],
  },
})

// Объедините MDX и конфигурацию Next.js
export default withMDX(nextConfig)

Фронтматтер

Фронтматтер — это YAML-подобные пары ключ/значение, которые можно использовать для хранения данных о странице. @next/mdx не поддерживает фронтматтер по умолчанию, но есть решения для его добавления:

Для доступа к метаданным страницы с @next/mdx вы можете экспортировать объект метаданных из .mdx-файла:

export const metadata = {
  author: 'Иван Иванов',
}

# Моя MDX-страница

Пользовательские элементы

Одно из преимуществ Markdown — соответствие нативным HTML-элементам, что делает написание быстрым и интуитивным:

Пример списка в Markdown:

- Один
- Два
- Три

Это генерирует следующий HTML:

<p>Пример списка в Markdown:</p>

<ul>
  <li>Один</li>
  <li>Два</li>
  <li>Три</li>
</ul>

Для стилизации элементов с уникальным оформлением можно использовать шорткоды — пользовательские компоненты, соответствующие HTML-элементам.

Для этого откройте файл mdx-components.tsx в корне приложения и добавьте пользовательские элементы:

import type { MDXComponents } from 'mdx/types'
import Image, { ImageProps } from 'next/image'

// Этот файл позволяет предоставлять пользовательские React-компоненты
// для использования в MDX-файлах. Можно импортировать и использовать любые
// React-компоненты, включая inline-стили,
// компоненты из других библиотек и т.д.

export function useMDXComponents(components: MDXComponents): MDXComponents {
  return {
    // Позволяет настраивать встроенные компоненты, например, для добавления стилей.
    h1: ({ children }) => <h1 style={{ fontSize: '100px' }}>{children}</h1>,
    img: (props) => (
      <Image
        sizes="100vw"
        style={{ width: '100%', height: 'auto' }}
        {...(props as ImageProps)}
      />
    ),
    ...components,
  }
}
import Image from 'next/image'

// Этот файл позволяет предоставлять пользовательские React-компоненты
// для использования в MDX-файлах. Можно импортировать и использовать любые
// React-компоненты, включая inline-стили,
// компоненты из других библиотек и т.д.

export function useMDXComponents(components) {
  return {
    // Позволяет настраивать встроенные компоненты, например, для добавления стилей.
    h1: ({ children }) => <h1 style={{ fontSize: '100px' }}>{children}</h1>,
    img: (props) => (
      <Image
        sizes="100vw"
        style={{ width: '100%', height: 'auto' }}
        {...props}
      />
    ),
    ...components,
  }
}

Подробнее: Как Markdown преобразуется в HTML?

React не понимает Markdown нативно. Текст Markdown сначала нужно преобразовать в HTML. Это можно сделать с помощью remark и rehype.

remark — это экосистема инструментов для работы с Markdown. rehype — аналогично, но для HTML. Например, следующий код преобразует Markdown в HTML:

import { unified } from 'unified'
import remarkParse from 'remark-parse'
import remarkRehype from 'remark-rehype'
import rehypeSanitize from 'rehype-sanitize'
import rehypeStringify from 'rehype-stringify'

main()

async function main() {
  const file = await unified()
    .use(remarkParse) // Преобразование в AST Markdown
    .use(remarkRehype) // Преобразование в AST HTML
    .use(rehypeSanitize) // Санитизация HTML
    .use(rehypeStringify) // Преобразование AST в HTML
    .process('Hello, Next.js!')

  console.log(String(file)) // <p>Hello, Next.js!</p>
}

Экосистемы remark и rehype включают плагины для подсветки синтаксиса, ссылок на заголовки, генерации оглавления и многого другого.

При использовании @next/mdx, как показано выше, вам не нужно работать с remark или rehype напрямую — это делается автоматически. Мы описываем это для более глубокого понимания работы @next/mdx.

Использование MDX-компилятора на Rust (экспериментально)

Next.js поддерживает новый MDX-компилятор на Rust. Он пока экспериментальный и не рекомендуется для продакшена. Для его использования нужно настроить next.config.js при передаче в withMDX:

next.config.js
module.exports = withMDX({
  experimental: {
    mdxRs: true,
  },
})

Полезные ссылки