MDX - это формат на основе Markdown, широко используемый в издательском деле благодаря своей легкости и простоте. Markdown - это легкий язык разметки с синтаксисом форматирования обычного текста. Он разработан так, чтобы его было легко писать и читать для пользователей, которые необязательно знакомы с синтаксисом HTML, хотя он широко используется для написания веб-страниц. В этой статье мы рассмотрим, как MDX может быть интегрирован в приложения Next.js.
Что такое Markdown?
Markdown - это простой и мощный инструмент для форматирования текстовых документов. Всего несколькими нажатиями клавиш вы можете создавать заголовки, подчеркивать слова, создавать списки и многое другое. Markdown легко читается и пишется, позволяя создавать богатые форматированные документы без использования сложного текстового процессора.
Если вы пишете в Интернете, в электронном письме или в файле программной документации, Markdown - ценный инструмент, который необходимо иметь в своем арсенале. В этой статье мы подробно рассмотрим, как легко интегрировать Markdown в Next.js.
Next.js и Markdown
Next.js - это JavaScript-фреймворк для создания серверных рендеринговых и статически генерируемых приложений. Одной из интересных особенностей Next.js является его интеграция с Markdown. С помощью Next.js вы можете легко импортировать и отображать файлы Markdown в своем приложении. Это может быть полезно при создании постов в блогах, документации или другого контента, требующего форматирования.
Чтобы использовать Markdown в Next.js, вам нужно установить необходимые зависимости, такие как remark и rehype-react. Затем вы будете использовать компонент remark-react для рендеринга контента в формате Markdown. Это позволит вам писать контент в простом и легко читаемом синтаксисе, а Next.js позаботится о рендеринге и форматировании.
В этом учебнике мы будем использовать next/mdx, next-mdx-remote и next-mdx-enhanced для реализации Markdown.
Реализация Markdown с помощью next/mdx
Чтобы начать работу, установите следующие зависимости:
npm install @next/mdx @mdx-js/loader @mdx-js/react
Чтобы настроить страницу, добавьте следующий код в файл next.config.js:
const nextConfig = {
reactStrictMode: true,
swcMinify: true,
};
const withMDX = require('@next/mdx')({
extension: /\.mdx?$/,
options: {
remarkPlugins: [],
rehypePlugins: [],
providerImportSource: '@mdx-js/react',
},
});
module.exports = nextConfig;
module.exports = withMDX({
pageExtensions: ['js', 'jsx', 'ts', 'tsx', 'md', 'mdx'],
});
После добавления приведенного выше кода в ваш проект Next.js каждый файл с расширением .mdx будет преобразован в страницу.
Далее создайте файл внутри каталога ./pages, например ./pages/blog/hello-world.mdx. Затем вы можете написать в этом файле Markdown, хотя он может показаться простым. Вот как это будет выглядеть:

Теперь мы создадим макет для страницы нашего блога, чтобы персонализировать наши HTML-компоненты. Создайте файл в месте ./layouts/Layout.tsx и введите в него следующий код:
import { MDXProvider } from '@mdx-js/react';
interface LayoutProps {
children: React.ReactNode;
}
function Layout ({children, ...props}: LayoutProps) {
return (
<MDXProvider components={components}>
{children}
</MDXProvider>
}
Затем добавьте этот компонент Layout на страницу Markdown:
import Layout from '../../components/layout';
# Hello World!
{/* Markdown content */}
export default ({ children }) => <Layout>{children}</Layout>;
Хотя сейчас ничего особенного не происходит, мы создали основу для настройки нашей страницы Markdown. Теперь давайте создадим несколько основных пользовательских компонентов, начиная с тегов заголовков.
Создание пользовательских компонентов
В ./components/mdx/Heading.tsx добавьте следующее:
export const Heading = {
H1: ({ children }) => <h1 className="text-2xl font-bold">{children}</h1>,
H2: ({ children }) => <h2 className="text-xl font-bold">{children}</h2>,
};
Аналогично, добавьте один для тега абзаца в ./components/mdx/Para.tsx:
function Para({ children }) {
return <p className="text-gray-700 my-4 text-base">{children}</p>;
}
export default Para;
Вы можете создавать более уникальные компоненты на основе спецификаций. Вы можете получить доступ к инструментам разработчика, нажав Ctrl + Shift + C. Вы можете навести курсор на элемент, который хотите изучить, чтобы увидеть, какие элементы обернуты вокруг него на сайте:

Далее перейдите к компоненту Layout и добавьте следующие изменения:
const components = {
h1: Heading.H1,
h2: Heading.H2,
p: Para,
ul: UnorderedList,
};
function Layout ({children, ...props}: LayoutProps) {
return (
<MDXProvider components={components}>
<div className="w-[80%] mx-auto p-6">
{children}
</div>
</MDXProvider>
)
}
Выглядит лучше!

Добавление метаданных
Еще один прием, который можно использовать с next/mdx, - добавление метаданных к содержимому Markdown. Это может быть полезно при добавлении SEO к вашей странице Markdown.
Теперь добавьте следующий код к странице Markdown, которую вы только что создали:
export const meta = {
author: 'Georgey',
title: 'Introduction to Technical Writing',
slug: 'introduction-to-technical-writing',
topics: [
'technical writing',
'software engineering',
'technical writing basics',
],
};
{/* Makrdown content */}
export default ({ children }) => <Layout meta={meta}>{children}</Layout>;
Теперь к этому объекту можно получить доступ с помощью компонента Layout.tsx, который мы создали ранее. В файле ./layouts/Layout.tsx добавьте содержимое метаданных в компонент Head, предоставленный Next.js:
// ...imports
import Head from "next/head"
interface LayoutProps {
children: React.ReactNode;
meta: { author: string; title: string; slug: string; topics: string[] };
}
function Layout ({children, ...props}: LayoutProps) {
return (
<MDXProvider components={components}>
<Head>
<title>{props.meta.title}</title>
<meta name="description" content={props.meta.title} />
</Head>
</MDXProvider>
)
}
После этого наша страница может быть проиндексирована веб-краулерами без проблем и заработать балл в рейтинге Lighthouse. Теперь, когда у нас есть метаданные, мы также можем включить их в раздел TL;DR страницы, чтобы предоставить читателям краткий обзор.
В компонент ./layouts/Layout.tsx добавьте следующее:
function Layout ({children, ...props}: LayoutProps) {
return (
<MDXProvider>
<div className="w-[80%] mx-auto p-6">
{/* Head */}
<div className="flex flex-col mt-6 mb-10 items-center justify-center text-center">
<h1 className="text-3xl font-bold">{props.meta.title}</h1>
<p className="text-md text-gray-500">By {props.meta.author}</p>
{/* topics */}
<div className="flex flex-wrap gap-2 mt-4">
{props.meta.topics.map((topic) => (
<span
key={topic}
className="text-sm text-gray-500 bg-gray-200 rounded-full px-2 py-1"
>
{topic.slice(0, 1).toUpperCase() + topic.slice(1)}
</span>
))}
</div>
</div>
{children}
</div>
</MDXProvider>
)
}
Чтобы помочь пользователю понять релевантность страницы, мы отобразили ключевые теги, к которым относится статья, в разделе выше. Вот как это будет выглядеть:

Интеграция MDX с next-mdx-remote
Чтобы увидеть код для этого метода в репозитории, измените ветку на using-next-mdx-remote, чтобы ссылаться на код для next-mdx-remote.
Чтобы начать работу с next-mdx-remote, установите указанную ниже зависимость:
npm install next-mdx-remote
Вы можете закомментировать конфигурации, которые мы добавили в файл next.config.js ранее для next/mdx. Для next-mdx-remote они нам не понадобятся.
Далее очистите файлы Markdown внутри каталога ./pages/blog и переместите их в отдельный каталог за пределами ./pages в каталог под названием ./database. Внутри каталога ./database создайте все файлы Markdown в отдельном каталоге для каждой страницы статьи.
Ваша файловая структура должна выглядеть следующим образом:
- pages
|- blog
|- [slug].tsx
- database
|- intro-to-technical-writing
|- intro-to-technical-writing.mdx
Этот формат файла будет полезен, когда мы будем добавлять в статьи элементы, например, изображения. После этого из файла Markdown и объекта метаданных нужно удалить строку экспорта. Теперь вы можете включить в свой Markdown-файл функцию под названием frontmatter.
Это будет служить метаданными нашей страницы и автоматически обрабатываться при получении содержимого Markdown:
---
title: Intro to technical writing
author: Georgey
topics: technical writing, writing, documentation
description: A short introduction to technical writing
---
# Intro to technical writing
Technical writing is the process of writing and sharing information in a professional setting. A technical writer, or tech writer, is a person who produces technical...
{/* ...content below */}
Осталось только настроить next-mdx-remote внутри файла [slug].tsx. Мы будем использовать функцию динамических маршрутов Next.js для создания страницы для каждого файла в каталоге ./database.
Мне больше нравится этот подход, поскольку он отделяет содержимое Markdown от кода извлечения данных, который имеет большее значение. Внутри [slug].tsx добавьте следующие строки:
import fs from "fs"
export async function getStaticPaths() {
const files = fs.readdirSync('database');
return {
paths: files.map((file) => ({
params: {
slug: file,
},
})),
fallback: false,
};
}
Динамические маршруты внутри ./pages должны быть указаны с количеством маршрутов, сгенерированных статически во время сборки. Для этого используется getStaticPaths. Мы можем использовать модуль файловой системы Node.js для получения имен файлов, которые будут использоваться в качестве имен маршрутов для каждой страницы статьи.
Как только это будет сделано, останется только получить конкретное содержимое Markdown. Для этого создайте функцию getStaticProps и включите в нее следующий код:
import { GetStaticPropsContext } from 'next';
import { serialize } from "next-mdx-remote/serialize"
import path from "path"
export async function getStaticProps(ctx: GetStaticPropsContext) {
const { slug } = ctx.params;
const source = fs.readFileSync(
path.join('database', slug as string, (slug + '.mdx') as string),
'utf8'
);
const mdxSource = await serialize(source, { parseFrontmatter: true });
return {
props: {
source: mdxSource,
},
};
}
В приведенном выше коде запрашиваемый slug используется в качестве параметра для получения содержимого Markdown из каталога ./database. Опять же, используя модуль fs, содержимое Markdown извлекается и сериализуется в JSX для использования парсером, предоставляемым next-mdx-remote.
Мы будем использовать его на стороне клиента в ближайшее время. Обратите внимание, что мы передаем дополнительный объект в функцию serialize со свойством parseFrontmatter, чтобы разобрать frontmatter из содержимого Markdown. Без этого вы могли бы получить пустой объект после сериализации.
Настройка клиентской части
Теперь перейдем к клиентской части. Добавьте следующий код, чтобы сразу увидеть Markdown:
import { GetStaticPropsContext, InferGetStaticPropsType } from 'next';
import { MDXRemote } from 'next-mdx-remote';
import Head from 'next/head';
function ArticlePage({
source,
}: InferGetStaticPropsType<typeof getStaticProps>) {
return (
<div>
<Head>
<title>{source.frontmatter.title}</title>
</Head>
<MDXRemote {...source} />
</div>
);
}
// getStaticPaths + getStaticProps
export default ArticlePage;

Как и раньше, давайте добавим несколько пользовательских компонентов к парсеру MDX. Это довольно просто сделать в next-mdx-remote:
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<MDXRemote
{...source}
components={{
h1: Heading.H1,
h2: Heading.H2,
p: Para,
ul: UnorderedList,
}}
/>
</div>
Затем импортируйте пользовательские компоненты, которые мы использовали ранее для @next/mdx:
import { Heading } from '../../components/mdx/Heading';
import Para from '../../components/mdx/Para';
import UnorderedList from '../../components/mdx/UnorderedList';
И, вуаля!

Использование react-markdown для интеграции MDX
Теперь мы рассмотрим использование react-markdown в качестве стратегии для интеграции MDX. Сначала установите следующие зависимости:
npm install react-markdown gray-matter
gray-matter - это пакет для разбора frontmatter, имеющегося на нашей странице, в отличие от встроенной функции serialize в next-mdx-remote.
Давайте перейдем к нашему файлу [slug].tsx. Функция getStaticPaths останется прежней, поскольку мы получаем имена файлов. В функции getStaticProps изменится только одна вещь, а именно замена функции serialize на gray-matter. gray-matter помогает разобрать содержимое метаданных, имеющихся на странице Markdown, и сериализовать его соответствующим образом.
Импортируйте gray-matter likewise и добавьте следующий код:
import matter from 'gray-matter';
export async function getStaticProps(ctx: GetStaticPropsContext) {
const { slug } = ctx.params;
const source = fs.readFileSync(
path.join('database', slug as string, (slug + '.md') as string),
'utf8'
);
const { data, content } = matter(source);
return {
props: {
data,
content,
},
};
}
Возвращаемое значение функции matter может быть деструктурировано для получения данных и содержимого. Обратите внимание, что мы будем использовать расширение файла .md для react-markdown вместо .mdx, поэтому обязательно внесите это изменение в метод path.join() выше. Далее перейдите к функции client и добавьте следующие строки:
import ReactMarkdown from "react-markdown"
function ArticlePage({
data,
content,
}: InferGetStaticPropsType<typeof getStaticProps>) {
return (
{/* header and layout */}
<Layout meta={data}>
<ReactMarkdown children={content} />
</Layout>
)
}
После этого ваша страница сможет отображать содержимое в формате Markdown. Далее, как обычно, добавим наши пользовательские компоненты. Опять же, это довольно просто в react-markdown:
<ReactMarkdown
children={content}
components={{
h1: Heading.H1,
h2: Heading.H2,
p: Para,
ul: UnorderedList,
}}
/>
Чтобы просмотреть полный список настраиваемых компонентов, посетите официальную страницу MDX.js.
Ленивая загрузка и оптимизация изображений
Наша статья также может содержать изображения, поэтому давайте настроим тег img на использование компонента Next.js Image, который позволяет ленивую загрузку и оптимизацию изображений.
Сначала создайте новый файл ./components/mdx/Image.tsx:
import Image from 'next/legacy/image';
function CustomImage({ src, alt, ...props }) {
return (
<div className="w-[10rem] p-10 mx-auto">
<Image
src={src}
width={300}
height={100}
layout="responsive"
alt={alt}
{...props}
/>
</div>
);
}
export default CustomImage;
Затем добавьте его в конфигурацию внутри компонента ReactMarkdown:
<ReactMarkdown
children={content}
components={{
h1: Heading.H1,
h2: Heading.H2,
p: Para,
ul: UnorderedList,
image: ({ src, alt, ...props }) => {
return <CustomImage src={src} alt={alt} {...props} />;
},
}}
/>
После этого вы сможете просмотреть изображение:

Добавление подсветки синтаксиса
Еще одна интересная функция, которую вы можете добавить в свои блоги, - подсветка синтаксиса для блоков кода. Для этого мы будем использовать react-syntax-highlighter. Чтобы установить его, используйте npm следующим образом:
npm install react-syntax-highlighter
Затем импортируйте Provider и тему, которую мы будем использовать для блоков кода:
// syntax-highlighter
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { atomDark } from 'react-syntax-highlighter/dist/cjs/styles/prism';
Внутри компонента ReactMarkdown добавьте еще один пользовательский компонент:
<ReactMarkdown
children={content}
components={{
...components,
code({ node, inline, className, children, ...props }) {
const match = /language-(\w+)/.exec(className || '');
return !inline && match ? (
<SyntaxHighlighter
children={String(children).replace(/\n$/, '')}
style={atomDark}
language={match[1]}
PreTag="div"
{...props}
/>
) : (
<span className={className} {...props}>
{children}
</span>
);
},
}}
/>
Эта строка кода проверяет содержимое Markdown на наличие одного тега кода ”” или блока кода, который обычно обернут в “---content---“”. Если это блок кода, он извлекает язык и присваивает его компоненту SyntaxHighlighter, добавляя соответствующую подсветку синтаксиса. После добавления этой функции вы должны увидеть следующую тему на ваших фрагментах кода:
```js
let name = "Georgey";
console.log("Hello " + name );
```

Заключение
В заключение следует отметить, что три различные стратегии интеграции MDX, описанные в этой статье, имеют уникальные преимущества и компромиссы. Добавив @next/mdx в свой проект Next.js, вы сможете легко писать JSX в своих Markdown-файлах и использовать преимущества автоматического разделения кода, предоставляемого Next.js.
next-mdx-remote позволяет отделить содержимое Markdown от кодовой базы и упростить управление. В то же время react-markdown предоставляет вам легкое решение для преобразования Markdown в JSX с минимальной настройкой. В конечном итоге наилучшая стратегия интеграции MDX для вашего проекта Next.js будет зависеть от ваших конкретных потребностей и требований.