Журнал / Разработка веб-продуктов
Как настроить динамические маршруты в Next.js. Полное разбор [[...slug]]
![Как настроить динамические маршруты в Next.js. Полное разбор [[...slug]]](/_next/image?url=%2Fimages%2Fblogs%2Fnext-routing-slug.webp&w=3840&q=75)
Динамические маршруты в App Router обеспечивают гибкость для создания блога, каталога, магазина или сайта документации. Основные случаи использования можно обработать с помощью [id] и [...slug], но когда речь идет о разделах с необязательным вложением, наиболее удобная архитектура обеспечивается [[...slug]].
В этой статье мы рассмотрим все три шаблона, но в основном сосредоточимся на [[...slug]]: как он соответствует URL-адресам, что приходит в params, как безопасно его типизировать, как генерировать статические пути, как строить хлебные крошки и как обрабатывать SEO.
Быстрая карта маршрутов
[slug]— один сегмент Пример:/blog/my-postparams.slug: строка[...slug]— обязательный catch-all (нужен хотя бы один сегмент) Пример:/docs/getting-started/installparams.slug: массив строк[[...slug]]— необязательный catch-all (соответствует как корню, так и любому количеству сегментов) Примеры:/shop,/shop/men,/shop/men/t-shirtsparams.slug: массив строк | неопределенный
Когда использовать [[...slug]]
- Один компонент и макет как для корня, так и для всех вложенных уровней раздела. Пример: документация или каталог, где главная страница раздела и подстраницы имеют одну и ту же структуру.
- Вам нужна корневая страница без сегментов, плюс неограниченное вложение ниже. Пример:
/docsкак содержание, а более глубокие пути, такие как/docs/guides/setup/cloud. - Хлебные крошки и навигация создаются из массива сегментов, но корневая страница также должна работать.
Базовая реализация [[...slug]]
Структура каталога:
app/
docs/
[[...slug]]/
page.tsx
layout.tsx
page.tsx:
import { notFound } from 'next/navigation';
type PageProps = {
params: { slug?: string[] };
};
async function getNodeByPath(slug: string[]) {
const path = '/' + slug.join('/');
const res = await fetch(`${process.env.API_URL}/docs?path=${encodeURIComponent(path)}`, {
cache: 'force-cache'
});
if (!res.ok) return null;
return res.json() as Promise<{ title: string; html: string } | null>;
}
export default async function DocsPage({ params }: PageProps) {
const segments = params.slug ?? []; // At the root, it's undefined, so convert to []
const node = await getNodeByPath(segments);
if (!node) notFound();
return (
<article>
<h1>{node.title}</h1>
<div dangerouslySetInnerHTML={{ __html: node.html }} />
</article>
);
}
Главные моменты:
- При соответствии
params.slugкорневому пути будетundefined. Сразу же преобразуйте его в[]. - Маршрут соответствует как
/docsтак и любым вложенным путям.
Хлебные крошки из [[...slug]]
import Link from 'next/link';
type CrumbsProps = { segments: string[] };
export function Breadcrumbs({ segments }: CrumbsProps) {
const items = [
{ label: 'Docs', href: '/docs' },
...segments.map((s, i) => ({
label: decodeURIComponent(s),
href: '/docs/' + segments.slice(0, i + 1).map(encodeURIComponent).join('/')
}))
];
return (
<nav aria-label="breadcrumb">
<ol>
{items.map(item => (
<li key={item.href}>
<Link href={item.href}>{item.label}</Link>
</li>
))}
</ol>
</nav>
);
}
Использование:
export default function DocsPage({ params }: { params: { slug?: string[] } }) {
const segments = params.slug ?? [];
return (
<>
<Breadcrumbs segments={segments} />
{/* content */}
</>
);
}
Лучшие практики:
- Всегда используйте
encodeURIComponentпри создании ссылок. - Для отображения вы можете декодировать, чтобы показать читаемые имена.
Генерация статических путей для [[...slug]]
Если вы знаете некоторые пути заранее и хотите предварительно построить их, используйте generateStaticParams. С необязательным catch-all вы можете генерировать как вложенные пути, так и корень.
export const revalidate = 300;
export async function generateStaticParams() {
return [
{ slug: [] }, // corresponds to /docs
{ slug: ['getting-started'] }, // corresponds to /docs/getting-started
{ slug: ['guides', 'install'] } // corresponds to /docs/guides/install
];
}
Примечания:
- Для
[[...slug]], удобно описать корневой путь как{ slug: [] }. Это позволяет четко указать, что должен быть сгенерирован маршрут без сегментов. - Чтобы строго ограничить допустимые статические пути, используйте:
SEO и канонические ссылки для [[...slug]]
generateMetadata получает params и позволяет генерировать метаданные на основе сегментов.
import type { Metadata } from 'next';
type Props = { params: { slug?: string[] } };
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const segments = params.slug ?? [];
const path = '/docs' + (segments.length ? '/' + segments.join('/') : '');
const res = await fetch(`${process.env.API_URL}/docs/meta?path=${encodeURIComponent(path)}`, {
cache: 'force-cache'
});
if (!res.ok) return { title: 'Documentation' };
const meta = await res.json() as { title: string; description?: string; canonical?: string };
return {
title: meta.title,
description: meta.description,
alternates: { canonical: meta.canonical ?? path },
openGraph: { title: meta.title, description: meta.description }
};
}
Рекомендации:
- Следите за дублирующими URL-адресами. Если вы используете
trailingSlash, убедитесь, что каноническая ссылка соответствует вашей фактической конфигурации. - Для страниц, основанных на данных, установите разумный период
revalidateчтобы метаданные оставались актуальными.
Управление кэшем и рендерингом
- Статический по умолчанию с ISR:export const revalidate = 300;
- Полностью динамический ответ:
await fetch(url, { cache: 'no-store' });
// или на уровне маршрута: export const dynamic = 'force-dynamic';
- Смешанный подход: кэшируйте некоторые данные, получайте другие данные свежими. Для часто обновляемых блоков рассмотрите маршруты собственных обработчиков.
[[...slug]] против [...slug] и общие ошибки
- Обработка корня
[[...slug]]соответствует и корню, и вложенным путям. На корнеparams.slugравенundefined. Приведите его к виду[].[...slug]никогда не совпадает с корнем. Требуется хотя бы один сегмент. - Набор типов Всегда явно указывайте тип:
params: { slug?: string[] }. Это предотвращает ошибки при доступе кparams.slug.length. - Статические пути Добавьте
{ slug: [] }если хотите предварительно построить корень. ПриdynamicParams = false, любой путь, не возвращенныйgenerateStaticParams, вызовет ошибку 404. - Ссылки Никогда не объединяйте сегменты напрямую. Используйте
encodeURIComponentпри создании href. - Конфликты маршрутов Группы маршрутов (например,
(маркетинг)) не отображаются в URL, но влияют на иерархию макета. Держите[[...slug]]изолированным в своем разделе, если у вас есть похожие шаблоны. - 404 Всегда вызывайте
notFound()для недопустимых путей. Настройтеapp/not-found.tsxдля удобства пользовательского интерфейса.
Полный пример раздела документации с [[...slug]]
app/
docs/
layout.tsx
[[...slug]]/
page.tsx
loading.tsx
not-found.tsx
layout.tsx:
export default function DocsLayout({ children }: { children: React.ReactNode }) {
return (
<div className="docs">
<aside>{/* section menu */}</aside>
<main>{children}</main>
</div>
);
}
page.tsx:
import { notFound } from 'next/navigation';
import { Breadcrumbs } from './_components/Breadcrumbs';
type PageProps = { params: { slug?: string[] } };
async function getDoc(path: string) {
const res = await fetch(`${process.env.API_URL}/docs?path=${encodeURIComponent(path)}`, {
next: { revalidate: 300 }
});
if (!res.ok) return null;
return res.json() as Promise<{ title: string; html: string } | null>;
}
export default async function DocsPage({ params }: PageProps) {
const segments = params.slug ?? [];
const path = '/docs' + (segments.length ? '/' + segments.join('/') : '');
const doc = await getDoc(path);
if (!doc) notFound();
return (
<article>
<Breadcrumbs segments={segments} />
<h1>{doc.title}</h1>
<div dangerouslySetInnerHTML={{ __html: doc.html }} />
</article>
);
}
generateStaticParams.ts:
not-found.tsx:
export default function NotFound() {
return <div>Page not found</div>;
}
Чек-лист предварительного релиза
/docsоткрывает главную страницу.- Глубокие пути, такие как
/docs/a/b/cкорректно рендерятся. params.slugприводится к виду[]на корне.- Хлебные крошки создают правильные закодированные ссылки.
generateMetadataвозвращает правильный заголовок и каноническую ссылку как для корневых, так и для вложенных страниц.notFound()работает для недопустимых узлов.revalidateиdynamicнастройки соответствуют требованиям к свежести.
Основные моменты
[[...slug]] обеспечивает чистую архитектуру для разделов, где корень и вложенные страницы разделяют одну страницу и макет. Он упрощает навигацию, хлебные крошки, SEO и общее обслуживание.
Самые важные моменты:
- Правильно типизируйте
params.slugи приведите его к виду[]на корне. - Безопасно создавайте ссылки с помощью
encodeURIComponent. - Предварительно построить пути с
{ slug: [] }при необходимости. - Управляйте SEO с помощью
generateMetadata. - Всегда возвращайте
notFound()для недопустимых путей.