Dynamic Routes та getStaticPaths
Next.js Dynamic Routes — бібліотека з пронумерованими полицями де бібліотекар шукає потрібну
У попередньому уроці ми навчилися отримувати дані через getStaticProps. Але як створити окрему SSG-сторінку для кожної статті блогу? Адже під час збірки Next.js повинен знати, які саме сторінки генерувати. Для цього існує getStaticPaths.
Проблема: динамічний SSG
Уяви, що у тебе блог з 50 статтями. Кожна стаття має свій URL: /blog/1, /blog/2, ..., /blog/50. З SSR (getServerSideProps) все просто -- сервер рендерить сторінку при кожному запиті. Але з SSG потрібно заздалегідь знати всі можливі шляхи.
getStaticPaths повертає масив всіх можливих параметрів, і Next.js генерує окрему HTML-сторінку для кожного з них.
getStaticPaths + getStaticProps
Ці дві функції працюють в парі:
// pages/blog/[id].tsx (Pages Router)
import type { GetStaticProps, GetStaticPaths, InferGetStaticPropsType } from "next";
interface Post {
id: number;
title: string;
body: string;
}
// Крок 1: Вказати які шляхи генерувати
export const getStaticPaths: GetStaticPaths = async () => {
const res = await fetch("https://jsonplaceholder.typicode.com/posts?_limit=10");
const posts: Post[] = await res.json();
const paths = posts.map((post) => ({
params: { id: String(post.id) }, // params мають бути рядками!
}));
return {
paths,
fallback: false, // 404 для неіснуючих шляхів
};
};
// Крок 2: Отримати дані для конкретної сторінки
export const getStaticProps: GetStaticProps<{ post: Post }> = async ({ params }) => {
const res = await fetch(
`https://jsonplaceholder.typicode.com/posts/${params?.id}`
);
const post: Post = await res.json();
return {
props: { post },
};
};
// Крок 3: Компонент сторінки
export default function BlogPostPage({
post,
}: InferGetStaticPropsType<typeof getStaticProps>) {
return (
<article>
<h1>{post.title}</h1>
<p>{post.body}</p>
</article>
);
}
Значення в params завжди мають бути рядками. Навіть якщо id -- це число, передавай String(post.id).
Що відбувається під час збірки:
- Next.js викликає
getStaticPaths→ отримує масив[{params: {id: "1"}}, {params: {id: "2"}}, ...] - Для кожного шляху викликає
getStaticPropsз відповіднимиparams - Генерує 10 HTML-файлів:
blog/1.html,blog/2.html, ...,blog/10.html
Fallback: що робити з неіснуючими сторінками?
Параметр fallback визначає, що відбувається, коли користувач відкриває шлях, якого немає в paths:
fallback: false
return {
paths,
fallback: false,
};
Будь-який шлях, якого немає в paths, повертає 404. Підходить, коли всі можливі шляхи відомі заздалегідь.
fallback: "blocking"
return {
paths,
fallback: "blocking",
};
Для невідомих шляхів Next.js генерує сторінку на льоту (SSR), потім кешує її як статичну:
- Перший запит до
/blog/999→ сервер генерує HTML (як SSR) - HTML кешується як статичний файл
- Наступні запити до
/blog/999→ отримують кешовану статику
fallback: true
return {
paths,
fallback: true,
};
Схоже на "blocking", але браузер одразу отримує сторінку з fallback-станом (без даних), а потім дані підвантажуються:
import { useRouter } from "next/router";
export default function BlogPostPage({ post }: { post: Post }) {
const router = useRouter();
// Показуємо лоадер, поки дані генеруються
if (router.isFallback) {
return <div>Завантаження...</div>;
}
return (
<article>
<h1>{post.title}</h1>
<p>{post.body}</p>
</article>
);
}
Використовуй fallback: "blocking" для більшості випадків -- це найпростіший варіант. fallback: true корисний, коли хочеш показати скелетон/лоадер одразу, не чекаючи генерації.
App Router: generateStaticParams
У сучасному App Router замість getStaticPaths використовується generateStaticParams -- простіша та елегантніша функція:
// src/app/blog/[slug]/page.tsx (App Router)
interface Post {
id: number;
title: string;
body: string;
}
// Замість getStaticPaths
export async function generateStaticParams() {
const res = await fetch("https://jsonplaceholder.typicode.com/posts?_limit=10");
const posts: Post[] = await res.json();
return posts.map((post) => ({
slug: String(post.id),
}));
}
// Замість getStaticProps -- просто async компонент
export default async function BlogPostPage({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const res = await fetch(
`https://jsonplaceholder.typicode.com/posts/${slug}`
);
const post: Post = await res.json();
return (
<article>
<h1>{post.title}</h1>
<p>{post.body}</p>
</article>
);
}
В App Router немає потреби у getStaticProps -- компонент сам є асинхронним і може отримувати дані напряму. Це набагато простіше!
Catch-all Routes
Для вкладених шляхів довільної глибини використовуй [...slug]:
src/app/
docs/
[...slug]/
page.tsx → /docs/a, /docs/a/b, /docs/a/b/c
// src/app/docs/[...slug]/page.tsx
export default async function DocsPage({
params,
}: {
params: Promise<{ slug: string[] }>;
}) {
const { slug } = await params;
// /docs/react/hooks/useState → slug = ["react", "hooks", "useState"]
return (
<div>
<h1>Документація</h1>
<p>Шлях: {slug.join(" / ")}</p>
</div>
);
}
Optional Catch-all: [[...slug]]
Подвійні дужки роблять параметр опціональним -- маршрут спрацює і без сегменту:
src/app/
docs/
[[...slug]]/
page.tsx → /docs, /docs/a, /docs/a/b
export default async function DocsPage({
params,
}: {
params: Promise<{ slug?: string[] }>;
}) {
const { slug } = await params;
// /docs → slug = undefined
// /docs/intro → slug = ["intro"]
if (!slug) {
return <h1>Головна сторінка документації</h1>;
}
return <h1>Документація: {slug.join(" / ")}</h1>;
}
Практичний приклад: блог з MDX
Уяви, що статті зберігаються як MDX-файли:
content/
posts/
hello-world.mdx
nextjs-intro.mdx
react-hooks.mdx
// src/lib/posts.ts
import fs from "fs";
import path from "path";
import matter from "gray-matter";
const postsDirectory = path.join(process.cwd(), "content/posts");
export interface PostMeta {
slug: string;
title: string;
date: string;
description: string;
}
export function getAllPostSlugs(): string[] {
const fileNames = fs.readdirSync(postsDirectory);
return fileNames.map((name) => name.replace(/\.mdx$/, ""));
}
export function getPostBySlug(slug: string): { meta: PostMeta; content: string } {
const fullPath = path.join(postsDirectory, `${slug}.mdx`);
const fileContents = fs.readFileSync(fullPath, "utf8");
const { data, content } = matter(fileContents);
return {
meta: {
slug,
title: data.title,
date: data.date,
description: data.description,
},
content,
};
}
// src/app/blog/[slug]/page.tsx
import { getAllPostSlugs, getPostBySlug } from "@/lib/posts";
export async function generateStaticParams() {
const slugs = getAllPostSlugs();
return slugs.map((slug) => ({ slug }));
}
export default async function PostPage({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const { meta, content } = getPostBySlug(slug);
return (
<article>
<h1>{meta.title}</h1>
<time>{meta.date}</time>
<div>{content}</div>
</article>
);
}
Підсумок
getStaticPaths(Pages Router) вказує Next.js, які динамічні сторінки генерувати під час збіркиfallback:false(404),"blocking"(SSR + кеш),true(з лоадером)generateStaticParams(App Router) -- сучасна заміна getStaticPaths- Catch-all routes
[...slug]-- для шляхів довільної глибини - Optional catch-all
[[...slug]]-- те ж, але необов'язковий сегмент
Що далі?
У наступному уроці вивчимо API Routes -- як створювати серверні endpoints прямо в Next.js, без окремого бекенду.
Корисні посилання:
- Next.js: Dynamic Routes -- документація App Router
- Next.js: generateStaticParams -- API reference
- gray-matter -- бібліотека для парсингу frontmatter