Введение/Руководства/MDX

Как использовать Markdown и MDX в Next.js

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, включая поддержку использования в Server Components (по умолчанию в App Router).

Полезно знать: Посмотрите Portfolio Starter Kit для полного рабочего примера.

Установка зависимостей

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

Установите эти пакеты для рендеринга MDX в Next.js:

Terminal
npm install @next/mdx @mdx-js/loader @mdx-js/react @types/mdx

Настройка next.config.mjs

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

next.config.mjs
import createMDX from '@next/mdx'

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

const withMDX = createMDX({
  // Добавьте плагины Markdown по желанию
})

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

Это позволяет файлам .mdx выступать в качестве страниц, маршрутов или импортов в вашем приложении.

Обработка .md файлов

По умолчанию next/mdx компилирует только файлы с расширением .mdx. Для обработки .md файлов с помощью webpack обновите опцию extension:

next.config.mjs
const withMDX = createMDX({
  extension: /\.(md|mdx)$/,
})

Полезно знать: Turbopack в настоящее время не поддерживает опцию extension и, следовательно, не поддерживает файлы .md.

Добавление файла mdx-components.tsx

Создайте файл mdx-components.tsx (или .js) в корне проекта для определения глобальных MDX-компонентов. Например, на том же уровне, что и pages или app, или внутри src, если применимо.

import type { MDXComponents } from 'mdx/types'

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

Полезно знать:

Рендеринг MDX

Вы можете рендерить MDX, используя файловую маршрутизацию Next.js или импортируя MDX-файлы в другие страницы.

Использование файловой маршрутизации

При использовании файловой маршрутизации вы можете использовать MDX-страницы как любые другие страницы.

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

  my-project
  |── mdx-components.(tsx/js)
  ├── pages
  │   └── mdx-page.(mdx/md)
  └── package.json

Вы можете использовать MDX в этих файлах и даже импортировать React-компоненты непосредственно в вашу MDX-страницу:

import { MyComponent } from 'my-component'

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

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

Это список в Markdown:

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

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

<MyComponent />

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

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

Создайте новую страницу в директории /pages и MDX-файл в любом удобном месте:

  .
  ├── markdown/
  │   └── welcome.(mdx/md)
  ├── pages/
  │   └── mdx-page.(tsx/js)
  ├── mdx-components.(tsx/js)
  └── package.json

Вы можете использовать MDX в этих файлах и даже импортировать React-компоненты непосредственно в вашу MDX-страницу:

import { MyComponent } from 'my-component'

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

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

Это список в Markdown:

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

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

<MyComponent />

Импортируйте MDX-файл внутрь страницы для отображения контента:

import Welcome from '@/markdown/welcome.mdx'

export default function Page() {
  return <Welcome />
}
import Welcome from '@/markdown/welcome.mdx'

export default function Page() {
  return <Welcome />
}

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

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

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

## Это заголовок

Это список в Markdown:

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

Генерирует следующий HTML:

<h2>Это заголовок</h2>

<p>Это список в Markdown:</p>

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

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

Глобальные стили и компоненты

Добавление стилей и компонентов в mdx-components.tsx повлияет на все MDX-файлы в вашем приложении.

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={{ color: 'red', fontSize: '48px' }}>{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={{ color: 'red', fontSize: '48px' }}>{children}</h1>
    ),
    img: (props) => (
      <Image
        sizes="100vw"
        style={{ width: '100%', height: 'auto' }}
        {...props}
      />
    ),
    ...components,
  }
}

Локальные стили и компоненты

Вы можете применять локальные стили и компоненты к конкретным страницам, передавая их в импортированные MDX-компоненты. Они объединятся и переопределят глобальные стили и компоненты.

import Welcome from '@/markdown/welcome.mdx'

function CustomH1({ children }) {
  return <h1 style={{ color: 'blue', fontSize: '100px' }}>{children}</h1>
}

const overrideComponents = {
  h1: CustomH1,
}

export default function Page() {
  return <Welcome components={overrideComponents} />
}
import Welcome from '@/markdown/welcome.mdx'

function CustomH1({ children }) {
  return <h1 style={{ color: 'blue', fontSize: '100px' }}>{children}</h1>
}

const overrideComponents = {
  h1: CustomH1,
}

export default function Page() {
  return <Welcome components={overrideComponents} />
}

Общие макеты

Для совместного использования макета между MDX-страницами создайте компонент макета:

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>
}

Затем импортируйте компонент макета в MDX-страницу, оберните MDX-контент в макет и экспортируйте его:

import MdxLayout from '../components/mdx-layout'

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

export default function MDXPage({ children }) {
  return <MdxLayout>{children}</MdxLayout>

}

Использование плагина Tailwind Typography

Если вы используете Tailwind для стилизации вашего приложения, плагин @tailwindcss/typography позволит вам повторно использовать конфигурацию и стили Tailwind в ваших markdown-файлах.

Плагин добавляет набор классов prose, которые можно применять для добавления типографических стилей к блокам контента, например, из markdown.

Установите Tailwind Typography и используйте общие макеты (shared layouts), чтобы добавить нужные вам prose-стили.

Чтобы создать общий макет для MDX-страниц, создайте компонент макета:

export default function MdxLayout({ children }: { children: React.ReactNode }) {
  // Создайте общий макет или стили здесь
  return (
    <div className="prose prose-headings:mt-8 prose-headings:font-semibold prose-headings:text-black prose-h1:text-5xl prose-h2:text-4xl prose-h3:text-3xl prose-h4:text-2xl prose-h5:text-xl prose-h6:text-lg dark:prose-headings:text-white">
      {children}
    </div>
  )
}
export default function MdxLayout({ children }) {
  // Создайте общий макет или стили здесь
  return (
    <div className="prose prose-headings:mt-8 prose-headings:font-semibold prose-headings:text-black prose-h1:text-5xl prose-h2:text-4xl prose-h3:text-3xl prose-h4:text-2xl prose-h5:text-xl prose-h6:text-lg dark:prose-headings:text-white">
      {children}
    </div>
  )
}

Затем импортируйте компонент макета в MDX-страницу, оберните MDX-контент в макет и экспортируйте его:

import MdxLayout from '../components/mdx-layout'

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

export default function MDXPage({ children }) {
  return <MdxLayout>{children}</MdxLayout>

}

Frontmatter

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

@next/mdx позволяет использовать экспорты, как и в любом другом JavaScript-компоненте:

export const metadata = {
  author: 'John Doe',
}

# Пост в блоге

Теперь метаданные можно использовать вне MDX-файла:

import BlogPost, { metadata } from '@/content/blog-post.mdx'

export default function Page() {
  console.log('metadata: ', metadata)
  //=> { author: 'John Doe' }
  return <BlogPost />
}
import BlogPost, { metadata } from '@/content/blog-post.mdx'

export default function Page() {
  console.log('metadata: ', metadata)
  //=> { author: 'John Doe' }
  return <BlogPost />
}

Распространённый вариант использования — итерация по коллекции MDX-файлов и извлечение данных. Например, создание индексной страницы блога из всех постов. Для этого можно использовать пакеты, такие как модуль fs Node.js или globby, чтобы прочитать директорию с постами и извлечь метаданные.

Полезно знать:

  • fs, globby и подобные инструменты можно использовать только на стороне сервера.
  • Посмотрите Portfolio Starter Kit для полного рабочего примера.

Плагины remark и rehype

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

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

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

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

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

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

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

Использование плагинов с Turbopack

Чтобы использовать плагины с Turbopack, обновите @next/mdx до последней версии и укажите имена плагинов в виде строки:

next.config.mjs
import createMDX from '@next/mdx'

/** @type {import('next').NextConfig} */
const nextConfig = {
  pageExtensions: ['js', 'jsx', 'md', 'mdx', 'ts', 'tsx'],
}

const withMDX = createMDX({
  options: {
    remarkPlugins: [],
    rehypePlugins: [['rehype-katex', { strict: true, throwOnError: true }]],
  },
})

export default withMDX(nextConfig)

Полезно знать:

Плагины remark и rehype без сериализуемых опций пока нельзя использовать с Turbopack из-за невозможности передавать JavaScript-функции в Rust.

Удалённый MDX

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

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

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

import {
  serialize,
  type SerializeResult,
} from 'next-mdx-remote-client/serialize'
import { MDXClient } from 'next-mdx-remote-client'

type Props = {
  mdxSource: SerializeResult
}

export default function RemoteMdxPage({ mdxSource }: Props) {
  if ('error' in mdxSource) {
    // Можно отобразить UI с ошибкой или выбросить `mdxSource.error`
  }
  return <MDXClient {...mdxSource} />
}

export async function getStaticProps() {
  // Текст MDX — может быть из базы данных, CMS, fetch и т. д.
  const res = await fetch('https:...')
  const mdxText = await res.text()
  const mdxSource = await serialize({ source: mdxText })
  return { props: { mdxSource } }
}
import { serialize } from 'next-mdx-remote-client/serialize'
import { MDXClient } from 'next-mdx-remote-client'

export default function RemoteMdxPage({ mdxSource }) {
  if ('error' in mdxSource) {
    // Можно отобразить UI с ошибкой или выбросить `mdXSource.error`
  }
  return <MDXClient {...mdxSource} />
}

export async function getStaticProps() {
  // Текст MDX — может быть из базы данных, CMS, fetch и т. д.
  const res = await fetch('https:...')
  const mdxText = await res.text()
  const mdxSource = await serialize({ source: mdxText })
  return { props: { mdxSource } }
}

Переход по маршруту /mdx-page-remote должен отобразить ваш скомпилированный MDX.

Подробнее: Как преобразовать 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,
  },
})

mdxRs также принимает объект для настройки преобразования MDX-файлов.

next.config.js
module.exports = withMDX({
  experimental: {
    mdxRs: {
      jsxRuntime?: string            // Кастомная jsx-среда выполнения
      jsxImportSource?: string       // Кастомный источник импорта jsx
      mdxType?: 'gfm' | 'commonmark' // Настройка синтаксиса MDX для парсинга и преобразования
    },
  },
})

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