Skip to content

Компоненты и страницы на Next.js с поддержкой Markdown

Продолжение серии: Архитектура модуля markdown для Jamstack на Next.js

Мы уже создали модуль для обработки Markdown. Теперь пора подключить его к интерфейсу. В этой статье мы:

markdown-pipeline.webp

  • создадим динамические страницы постов
  • добавим список всех постов
  • подключим данные из Markdown в компоненты
  • применим SSG для генерации страниц на этапе сборки
  1. Первым делом делаем новую ветку:
git checkout -b rendering-markdown-in-next
  1. Добавим несколько постов. По аналогии с первым.

Посты в Markdown

Расширение может быть как md, так и mdx.

  1. Почистим проект от лишнего контента. Удалим все файлы из public и favicon.ico из app

Максимально упростим app/layout.tsx:

import { Inter } from "next/font/google";
import { ReactNode } from "react";
 
import "./globals.css";
 
const inter = Inter({ subsets: ["latin", "cyrillic"] });
 
export default function RootLayout({ children }: { children: ReactNode }) {
  return (
    <html lang="en">
      <body className={inter.className}>
        <main className="container mx-auto px-4">{children}</main>
      </body>
    </html>
  );
}

То же самое и с app/page.tsx:

import Link from "next/link";
 
import { getPosts } from "@/app/lib/markdown";
 
export default async function Home() {
  const posts = await getPosts();
 
  return (
    <div className="container mx-auto px-4">
      <h1 className="mb-6 text-4xl font-bold">My MDX Blog</h1>
      <p className="mb-4 text-lg">
        Explore my latest articles and deep dives into various topics.
      </p>
      <Link
        href="posts"
        className="text-blue-500 hover:underline dark:text-blue-400"
      >
        View All Posts →
      </Link>
      <div className="mt-6 flex flex-col gap-4">
        {posts.map((post) => (
          <Link
            key={post.slug}
            href={`/posts/${post.slug}`}
            className="block rounded-lg p-4 shadow-sm transition hover:bg-gray-200 dark:hover:bg-gray-700"
          >
            <h2 className="text-2xl font-semibold">{post.title}</h2>
          </Link>
        ))}
      </div>
    </div>
  );
}
  1. Проверяем результат. При запуске npm run dev мы видим список постов. переходя по постам пока должна быть ошибка. Но главная страница сайта должна отображаться вот так:

Главная страница

Ссылки на работающую ветку с кодом к посту можно всегда посмотреть внизу каждого шага инструкции. На случай если плохо получается. Если всё получается ок, то идём дальше.

Теперь подходим к главному

  1. Динамическая страница

Создаём внутреннюю страницу для всех постов и для каждого в отдельности:

── app/
── posts/               #  можете назвать blog, articles и т.д.
    ├──[slug]           #  так и делаем в квадратных скобках, так мы будем ловить все динамические маршруты
     └──page.tsx   #  template для внутренней страницы
    └──page.tsx         #  страница раздела, например новости, или документация.
  1. Внутренняя страница app/posts/[slug]/page.tsx:
// app/posts/[slug]/page.tsx
import Link from "next/link";
import { notFound } from "next/navigation";
 
import { getPostBySlug, getPosts } from "@/app/lib/markdown";
 
export default async function ContentPage({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params;
  const post = await getPostBySlug(slug);
 
  if (!post) {
    notFound();
  }
 
  return (
    <div className="container mx-auto px-4 py-8">
      <Link href="/posts" className="mb-4 block text-blue-600 hover:underline">
        ← Back to all posts
      </Link>
      <article>
        <h1 className="mb-6 text-4xl font-bold">{post.title}</h1>
        <div
          // className="Здесь позже будут добавлены классы для темизации"
          dangerouslySetInnerHTML={{ __html: post.content }}
        />
      </article>
    </div>
  );
}
 
export async function generateStaticParams() {
  const posts = await getPosts();
  return posts.map((post) => ({
    slug: post.slug,
  }));
}

Достаточно важный момент обернуть наш импортируемый компонент в dangerouslySetInnerHTML <div dangerouslySetInnerHTML={{ __html: post.content }} />. Это позволяет нам вставить HTML-код, который мы получили из Markdown. Это важно, потому что мы не можем просто вставить HTML-код в JSX, так как он будет экранирован.

Общая страница для раздела - страница app/posts/page.tsx:

import Link from "next/link";
 
import { getPosts } from "@/app/lib/markdown";
 
export default async function PostsPage() {
  const posts = await getPosts();
 
  return (
    <div className="container mx-auto px-4 py-8">
      <h1 className="mb-6 text-4xl font-bold">All Posts</h1>
      <p className="mb-4 text-lg">
        Browse through all my articles and discover something new.
      </p>
      <div className="space-y-4">
        {posts.map((post) => (
          <Link
            key={post.slug}
            href={`/posts/${post.slug}`}
            className="block rounded-lg p-4 shadow-sm transition hover:bg-gray-200"
          >
            <h2 className="text-2xl font-semibold">{post.title}</h2>
          </Link>
        ))}
      </div>
    </div>
  );
}
  1. Проверяем результат. При запуске npm run dev мы видим список постов. переходя по постам мы видим их контент.

Страница должна выглядеть примерно вот так:

Страница поста

Если у вас такой результат, то всё прекрасно. Мы считали наш контент правильно. Теперь остаётся юстировать его.

  1. Нам надо вернуться в app/lib/markdown/services/remark-markdown-processor.service.ts и там добавить новые возможности.
const result = await remark().process(markdown);
// эту строку мы улучшим.
const result = await remark()
  .use(gfm)
  .use(remarkRehype)
  .use(rehypePrettyCode, {
    theme: "rose-pine-dawn",
    keepBackground: true,
  })
  .use(rehypeStringify)
  .process(markdown);

Внутри html markdown

.use(gfm) от remark-gfm включает поддержку GitHub Flavored Markdown:

  • Таблицы (|)
  • Чекбоксы (- [ ])
  • Автоматические ссылки
  • Удаление текста (удалённый)
  • Без этого remark не понимает расширения Markdown от GitHub.

.use(rehypePrettyCode, {...}) от rehype-pretty-code это подсветка кода в блоках (js ... ). Использует темы, основанные на Shiki. В параметрах:

  • theme: задаёт цветовую схему (например, rose-pine-dawn)
  • keepBackground: true — сохраняет цвет фона блока

Полный список тем с предосмотром - тут

.use(remarkRehype) от remark-rehype преобразует Markdown AST (remark) в HTML AST (rehype). Это "мост" между markdown и HTML.

.use(rehypeStringify) от rehype-stringify преобразует HTML AST в HTML-строку. Это финальный шаг: ты получаешь HTML, готовый к вставке в страницу или рендеру.

шаг с темизацией

Принципиально остался только шаг с темизацией. И мы рассмотрим его и тёмную со светлой темой в следующем шаге.

15 апр. 2025 г.
andron13