Урок 02

Динамические маршруты

Сегменты в [квадратных скобках] становятся параметрами.

Базовый динамический сегмент

app/posts/[id]/page.tsx:

export default async function Post({
    params,
}: {
    params: Promise<{ id: string }>;
}) {
    const { id } = await params;
    return <h1>Пост {id}</h1>;
}

URL: /posts/1, /posts/hello, /posts/anything.

Несколько параметров

app/[category]/[id]/page.tsx:

export default async function Page({
    params,
}: {
    params: Promise<{ category: string; id: string }>;
}) {
    const { category, id } = await params;
    return <p>{category} / {id}</p>;
}

Типы

Все параметры — строки. Если нужен number — преобразуй:

const { id } = await params;
const postId = Number(id);

if (isNaN(postId)) notFound();

Catch-all сегмент

[...slug] — ловит любое количество сегментов:

app/docs/[...slug]/page.tsx:

export default async function Docs({
    params,
}: {
    params: Promise<{ slug: string[] }>;
}) {
    const { slug } = await params;
    return <p>Путь: {slug.join('/')}</p>;
}
  • /docs/a → ['a']
  • /docs/a/b → ['a', 'b']
  • /docs/a/b/c → ['a', 'b', 'c']

Но не /docs (без сегментов).

Опциональный catch-all

[[...slug]] — ловит и без сегментов:

app/docs/[[...slug]]/page.tsx:

const { slug } = await params;
// /docs → undefined
// /docs/a → ['a']

Проверка:

const path = slug?.join('/') ?? 'главная';

generateStaticParams

Предгенерация статических страниц:

export async function generateStaticParams() {
    const posts = await getPosts();

    return posts.map((post) => ({
        id: String(post.id),
    }));
}

Next.js сгенерирует HTML для всех этих URL на этапе сборки.

Для нескольких параметров:

export async function generateStaticParams() {
    return [
        { category: 'tech', id: '1' },
        { category: 'life', id: '2' },
    ];
}

dynamicParams

Если URL не в generateStaticParams:

export const dynamicParams = true; // по умолчанию — генерируется на лету

false — 404 для всех остальных.

Пример: блог

app/
├── blog/
│   ├── page.tsx              → /blog
│   └── [slug]/
│       └── page.tsx          → /blog/my-post
// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
    const posts = await getPosts();
    return posts.map((p) => ({ slug: p.slug }));
}

export default async function Post({ params }) {
    const { slug } = await params;
    const post = await getPostBySlug(slug);

    if (!post) notFound();

    return <article>{post.content}</article>;
}

Пример: документация

app/docs/[[...slug]]/page.tsx
  • /docs — главная документации
  • /docs/getting-started — один раздел
  • /docs/api/users — вложенный путь

notFound

import { notFound } from 'next/navigation';

const post = await getPost(id);
if (!post) notFound();

Показывает ближайший not-found.tsx.

Валидация параметров

const { id } = await params;
const postId = Number(id);

if (!Number.isInteger(postId) || postId <= 0) {
    notFound();
}

Итоги

  • [id] — динамический сегмент
  • [...slug] — catch-all
  • [[...slug]] — опциональный
  • params — промис
  • Все параметры — строки
  • generateStaticParams для SSG
  • notFound() для 404