![Jak skonfigurować dynamiczny routing w Next.js. Pełne omówienie [[...slug]]](/images/uploads/next-routing-slug.webp)
Jak skonfigurować dynamiczny routing w Next.js. Pełne omówienie [[...slug]]
Dynamiczny routing w App Router daje dużą elastyczność przy budowie bloga, katalogu, sklepu czy dokumentacji. Podstawowe przypadki można obsłużyć za pomocą [id]
oraz [...slug]
, ale gdy chodzi o sekcje z opcjonalnym zagnieżdżeniem, najwygodniejszą architekturę zapewnia [[...slug]]
.
W tym artykule omówimy wszystkie trzy warianty, a największą uwagę poświęcimy [[...slug]]
: jak dopasowuje adresy URL, co trafia do params
, jak poprawnie go typować, jak generować statyczne ścieżki, budować breadcrumbs i zadbać o SEO.
Krótka mapa routingu
[slug]
— jeden segment Przykład:/blog/my-post
params.slug: string
[...slug]
— wymagany catch-all (musi być przynajmniej jeden segment) Przykład:/docs/getting-started/install
params.slug: string[]
[[...slug]]
— opcjonalny catch-all (pasuje zarówno do korzenia, jak i do dowolnej liczby segmentów) Przykłady:/shop
,/shop/men
,/shop/men/t-shirts
params.slug: string[] | undefined
Kiedy używać [[...slug]]
- Jeden komponent i layout zarówno dla strony głównej sekcji, jak i wszystkich podstron. Przykład: dokumentacja lub katalog, gdzie
/docs
i/docs/setup/installation
mają tę samą strukturę. - Potrzebujesz strony głównej bez segmentów oraz nieograniczonego zagnieżdżania niżej. Przykład:
/docs
jako spis treści i dalsze ścieżki jak/docs/guides/setup/cloud
. - Breadcrumbs i nawigacja są budowane z tablicy segmentów, a strona główna sekcji też musi działać.
Podstawowa implementacja [[...slug]]
Struktura katalogów:
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 ?? []; // w korzeniu będzie undefined, więc zamieniamy na []
const node = await getNodeByPath(segments);
if (!node) notFound();
return (
<article>
<h1>{node.title}</h1>
<div dangerouslySetInnerHTML={{ __html: node.html }} />
</article>
);
}
Kluczowe punkty:
- W korzeniu
params.slug
jestundefined
. Zamień na[]
. - Ten sam routing obsługuje zarówno
/docs
, jak i dowolne zagnieżdżone ścieżki.
Breadcrumbs z [[...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>
);
}
Użycie:
export default function DocsPage({ params }: { params: { slug?: string[] } }) {
const segments = params.slug ?? [];
return (
<>
<Breadcrumbs segments={segments} />
{/* zawartość */}
</>
);
}
Dobre praktyki:
- Przy budowie linków zawsze używaj
encodeURIComponent
. - Do wyświetlania możesz dekodować, aby pokazać czytelne nazwy.
Generowanie statycznych ścieżek dla [[...slug]]
Jeśli znasz część ścieżek i chcesz je zbudować statycznie, użyj generateStaticParams
.
export const revalidate = 300;
export async function generateStaticParams() {
return [
{ slug: [] }, // odpowiada /docs
{ slug: ['getting-started'] }, // odpowiada /docs/getting-started
{ slug: ['guides', 'install'] } // odpowiada /docs/guides/install
];
}
Uwagi:
- Dla
[[...slug]]
ścieżkę korzenia opisujesz jako{ slug: [] }
. - Jeśli chcesz ograniczyć routing tylko do podanych ścieżek:
export const dynamicParams = false; // wszystko inne zwróci 404
SEO i linki kanoniczne dla [[...slug]]
generateMetadata
pozwala opisać metadane na podstawie segmentów:
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: 'Dokumentacja' };
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 }
};
}
Rekomendacje:
- Uważaj na duplikaty adresów URL. Jeśli masz włączone
trailingSlash
, zadbaj o zgodność z canonical. - Dla stron dynamicznych ustaw rozsądny
revalidate
.
Kontrola cache i renderowania
- Domyślnie routing jest statyczny + ISR:export const revalidate = 300;
- Całkowicie dynamiczna odpowiedź:
await fetch(url, { cache: 'no-store' });
- Hybryda: część danych z cache, część świeżo pobierana.
[[...slug]]
vs [...slug]
i typowe pułapki
- Korzeń sekcji
[[...slug]]
pasuje i do korzenia, i do zagnieżdżeń.[...slug]
nigdy nie pasuje do korzenia. - Typowanie
Zawsze określaj typ:
params: { slug?: string[] }
. - Ścieżki statyczne
Dodaj
{ slug: [] }
jeśli chcesz zbudować korzeń. - Linki
Nigdy nie konkatenatuj segmentów ręcznie, używaj
encodeURIComponent
. - Konflikty routingu
Grupy
(marketing)
nie są w URL, ale wpływają na layout. - 404
Zawsze wywołuj
notFound()
dla nieistniejących kombinacji.
Kompletny przykład sekcji dokumentacji z [[...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>{/* menu sekcji */}</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
:
export const revalidate = 300;
export const dynamicParams = true; // pozwól na ścieżki nieopisane
export async function generateStaticParams() {
return [
{ slug: [] }, // /docs
{ slug: ['getting-started'] }, // /docs/getting-started
{ slug: ['guides', 'install'] } // /docs/guides/install
];
}
not-found.tsx
:
export default function NotFound() {
return <div>Strona nie znaleziona</div>;
}
Checklist przed publikacją
/docs
otwiera stronę główną./docs/a/b/c
działa poprawnie.params.slug
w korzeniu jest normalizowany do[]
.- Breadcrumbs budują poprawne linki.
generateMetadata
zwraca właściwe title i canonical.notFound()
działa dla złych ścieżek.- Ustawienia
revalidate
idynamic
odpowiadają wymaganiom.
Co warto zapamiętać
[[...slug]]
daje czystą architekturę, gdzie zarówno korzeń, jak i wszystkie poziomy zagnieżdżenia są obsługiwane jednym routingiem i layoutem. Upraszcza to nawigację, breadcrumbs, SEO i utrzymanie projektu.
Najważniejsze:
- Typuj
params.slug
i normalizuj do[]
. - Buduj linki z
encodeURIComponent
. - Dodaj
{ slug: [] }
do statycznych ścieżek, jeśli chcesz prebuildować korzeń. - Generuj SEO w
generateMetadata
. - Zwracaj
notFound()
dla niepoprawnych adresów.