Стратегии интеграции MDX с Next.js

Стратегии интеграции MDX с Next.js

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, хотя он может показаться простым. Вот как это будет выглядеть:

Пример блога, демонстрирующий интеграцию MDX в Next.js

Теперь мы создадим макет для страницы нашего блога, чтобы персонализировать наши 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. Вы можете навести курсор на элемент, который хотите изучить, чтобы увидеть, какие элементы обернуты вокруг него на сайте:

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

Далее перейдите к компоненту 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>
  )
}

Выглядит лучше!

Компоненты в MDX и Next.js Blog

Добавление метаданных

Еще один прием, который можно использовать с 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 and Next.js Integration Adding Metadata

Интеграция 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.js с MDX-Remote

Как и раньше, давайте добавим несколько пользовательских компонентов к парсеру 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';

И, вуаля!

Улучшенный результат интеграции MDX в Next.js

Использование 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} />;
    },
  }}
/>

После этого вы сможете просмотреть изображение:

Добавление изображений в блог с помощью MDX и Next.js

Добавление подсветки синтаксиса

Еще одна интересная функция, которую вы можете добавить в свои блоги, - подсветка синтаксиса для блоков кода. Для этого мы будем использовать 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 );
```
Next.js Image Optimization

Заключение

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

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