Компоненты и страницы на Next.js с поддержкой Markdown
Продолжение серии: Архитектура модуля markdown для Jamstack на Next.js
Мы уже создали модуль для обработки Markdown. Теперь пора подключить его к интерфейсу. В этой статье мы:
- создадим динамические страницы постов
- добавим список всех постов
- подключим данные из Markdown в компоненты
- применим SSG для генерации страниц на этапе сборки
- Первым делом делаем новую ветку:
git checkout -b rendering-markdown-in-next
- Добавим несколько постов. По аналогии с первым.
Расширение может быть как md, так и mdx.
- Почистим проект от лишнего контента. Удалим все файлы из
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>
);
}
- Проверяем результат. При запуске
npm run dev
мы видим список постов. переходя по постам пока должна быть ошибка. Но главная страница сайта должна отображаться вот так:
Ссылки на работающую ветку с кодом к посту можно всегда посмотреть внизу каждого шага инструкции. На случай если плохо получается. Если всё получается ок, то идём дальше.
Теперь подходим к главному
- Динамическая страница
Создаём внутреннюю страницу для всех постов и для каждого в отдельности:
── app/
── posts/ # можете назвать blog, articles и т.д.
├──[slug] # так и делаем в квадратных скобках, так мы будем ловить все динамические маршруты
│ └──page.tsx # template для внутренней страницы
└──page.tsx # страница раздела, например новости, или документация.
- Внутренняя страница
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>
);
}
- Проверяем результат. При запуске
npm run dev
мы видим список постов. переходя по постам мы видим их контент.
Страница должна выглядеть примерно вот так:
Если у вас такой результат, то всё прекрасно. Мы считали наш контент правильно. Теперь остаётся юстировать его.
- Нам надо вернуться в
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);
.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, готовый к вставке в страницу или рендеру.
Принципиально остался только шаг с темизацией. И мы рассмотрим его и тёмную со светлой темой в следующем шаге.