Использование 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:
npm install @next/mdx @mdx-js/loader @mdx-js/react @types/mdx
Настройка next.config.mjs
Обновите файл next.config.mjs
в корне проекта для настройки работы с MDX:
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
:
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-components.tsx
обязателен для использования@next/mdx
с App Router и не будет работать без него.- Узнайте больше о конвенции файла
mdx-components.tsx
.- Узнайте, как использовать пользовательские стили и компоненты.
Рендеринг MDX
Вы можете рендерить MDX, используя файловую маршрутизацию Next.js или импортируя MDX-файлы в другие страницы.
Использование файловой маршрутизации
При использовании файловой маршрутизации вы можете использовать MDX-страницы как любые другие страницы.
В приложениях с App Router это включает возможность использования метаданных.
Создайте новую MDX-страницу в директории /app
:
my-project
├── app
│ └── mdx-page
│ └── page.(mdx/md)
|── mdx-components.(tsx/js)
└── package.json
Вы можете использовать MDX в этих файлах и даже импортировать React-компоненты непосредственно в вашу MDX-страницу:
import { MyComponent } from 'my-component'
# Добро пожаловать на мою MDX-страницу!
Это **жирный** и _курсивный_ текст.
Это список в Markdown:
- Один
- Два
- Три
Посмотрите мой React-компонент:
<MyComponent />
Переход по маршруту /mdx-page
должен отобразить вашу MDX-страницу.
Использование импортов
Создайте новую страницу в директории /app
и MDX-файл в любом удобном месте:
.
├── app/
│ └── mdx-page/
│ └── page.(tsx/js)
├── markdown/
│ └── welcome.(mdx/md)
├── 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-страницу.
Использование динамических импортов
Вы можете импортировать динамические MDX-компоненты вместо использования файловой маршрутизации для MDX-файлов.
Например, у вас может быть динамический сегмент маршрута, который загружает MDX-компоненты из отдельной директории:

generateStaticParams
может использоваться для предварительного рендеринга указанных маршрутов. Установив dynamicParams
в false
, доступ к маршруту, не определенному в generateStaticParams
, вернет 404.
export default async function Page({
params,
}: {
params: Promise<{ slug: string }>
}) {
const { slug } = await params
const { default: Post } = await import(`@/content/${slug}.mdx`)
return <Post />
}
export function generateStaticParams() {
return [{ slug: 'welcome' }, { slug: 'about' }]
}
export const dynamicParams = false
export default async function Page({ params }) {
const { slug } = await params
const { default: Post } = await import(`@/content/${slug}.mdx`)
return <Post />
}
export function generateStaticParams() {
return [{ slug: 'welcome' }, { slug: 'about' }]
}
export const dynamicParams = false
Полезно знать: Убедитесь, что вы указали расширение
.mdx
в вашем импорте. Хотя не обязательно использовать алиасы путей модулей (например,@/content
), это упрощает путь импорта.
Использование пользовательских стилей и компонентов
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-страницами вы можете использовать встроенную поддержку макетов в 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>
}
Использование плагина Tailwind Typography
Если вы используете Tailwind для стилизации вашего приложения, плагин @tailwindcss/typography
позволит вам повторно использовать конфигурацию и стили Tailwind в ваших markdown-файлах.
Плагин добавляет набор классов prose
, которые можно применять для добавления типографических стилей к блокам контента, например, из markdown.
Установите Tailwind Typography и используйте общие макеты (shared layouts), чтобы добавить нужные вам prose
-стили.
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>
)
}
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
в качестве файла конфигурации.
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
до последней версии и укажите имена плагинов в виде строки:
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 { MDXRemote } from 'next-mdx-remote-client/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-client/rsc'
export default async function RemoteMdxPage() {
// Текст MDX — может быть из базы данных, CMS, fetch и т. д.
const res = await fetch('https://...')
const markdown = await res.text()
return <MDXRemote source={markdown} />
}
Переход по маршруту /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
:
module.exports = withMDX({
experimental: {
mdxRs: true,
},
})
mdxRs
также принимает объект для настройки преобразования MDX-файлов.
module.exports = withMDX({
experimental: {
mdxRs: {
jsxRuntime?: string // Кастомная jsx-среда выполнения
jsxImportSource?: string // Кастомный источник импорта jsx
mdxType?: 'gfm' | 'commonmark' // Настройка синтаксиса MDX для парсинга и преобразования
},
},
})