Journal / Entwicklung von Webprodukten
Dynamische Routen in Next.js einrichten: der vollständige Überblick über [[...slug]]
![Dynamische Routen in Next.js einrichten: der vollständige Überblick über [[...slug]]](/_next/image?url=%2Fimages%2Fblogs%2Fnext-routing-slug.webp&w=3840&q=75)
Dynamisches Routing im App Router bietet Flexibilität beim Aufbau eines Blogs, Katalogs, Shops oder einer Dokumentationsseite. Die Grundfälle lassen sich mit [id] und [...slug] abdecken, doch bei Bereichen mit optionaler Verschachtelung bietet [[...slug]] die bequemste Architektur.
In diesem Artikel behandeln wir alle drei Muster, konzentrieren uns aber vor allem auf [[...slug]]: wie es zu URLs passt, was in params ankommt, wie Sie es sicher deklarieren, wie Sie statische Pfade erzeugen, wie Sie Breadcrumbs bauen und wie Sie SEO umsetzen.
Kurzer Überblick
[slug]– ein einzelnes Segment. Beispiel:/blog/my-post.params.slug: string[...slug]– verpflichtendes Catch-all (mindestens ein Segment ist nötig). Beispiel:/docs/getting-started/install.params.slug: string[][[...slug]]– optionales Catch-all (passt sowohl auf die Wurzel als auch auf eine beliebige Anzahl von Segmenten). Beispiele:/shop,/shop/men,/shop/men/t-shirts.params.slug: string[] | undefined
Wann Sie [[...slug]] einsetzen
- Eine Komponente und ein Layout für die Wurzel und alle verschachtelten Ebenen eines Bereichs. Beispiel: Dokumentation oder Katalog, in dem die Startseite des Bereichs und die Unterseiten dieselbe Struktur haben.
- Sie brauchen eine Wurzelseite ohne Segmente plus unbegrenzte Verschachtelung darunter. Beispiel:
/docsals Inhaltsverzeichnis und tiefere Pfade wie/docs/guides/setup/cloud. - Breadcrumbs und Navigation werden aus dem Array der Segmente gebaut, aber auch die Wurzelseite muss funktionieren.
Grundlegende Implementierung von [[...slug]]
Verzeichnisstruktur:
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>
);
}
Die wichtigsten Punkte:
- An der Wurzel ist
params.sluggleichundefined. Wandeln Sie es sofort in[]um. - Die Route passt sowohl auf
/docsals auch auf beliebige Verschachtelungen.
Breadcrumbs aus [[...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>
);
}
Verwendung:
export default function DocsPage({ params }: { params: { slug?: string[] } }) {
const segments = params.slug ?? [];
return (
<>
<Breadcrumbs segments={segments} />
{/* content */}
</>
);
}
Bewährte Praktiken:
- Verwenden Sie beim Erzeugen von Links immer
encodeURIComponent. - Für die Anzeige können Sie dekodieren, um menschenlesbare Namen zu zeigen.
Statische Pfade für [[...slug]] erzeugen
Kennen Sie einige Pfade im Voraus und möchten sie vorab bauen, nutzen Sie generateStaticParams. Mit dem optionalen Catch-all können Sie sowohl verschachtelte Pfade als auch die Wurzel erzeugen.
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
];
}
Hinweise:
- Für
[[...slug]]ist es praktisch, den Wurzelpfad als{ slug: [] }zu beschreiben. Das zeigt klar, dass ein Pfad ohne Segmente erzeugt werden muss. - Um die gültigen statischen Pfade strikt zu begrenzen, verwenden Sie:
SEO und Canonical-Links für [[...slug]]
generateMetadata erhält params und erlaubt es, Metadaten anhand der Segmente zu erzeugen.
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 }
};
}
Empfehlungen:
- Seien Sie vorsichtig mit doppelten URLs. Wenn Sie
trailingSlashverwenden, stellen Sie sicher, dass das Canonical zur tatsächlichen Konfiguration passt. - Setzen Sie bei datenbasierten Seiten ein vernünftiges
revalidate, damit die Metadaten immer aktuell sind.
Steuerung von Cache und Rendering
- Standardmäßig statisch mit ISR:
export const revalidate = 300; - Vollständig dynamische Antwort:
await fetch(url, { cache: 'no-store' });
// oder auf Routenebene: export const dynamic = 'force-dynamic';
- Gemischter Ansatz: Manche Daten cachen, andere Daten laufend abrufen. Für häufig aktualisierte Blöcke erwägen Sie Route Handlers mit eigenem Cache.
[[...slug]] vs. [...slug] und typische Fallstricke
- Behandlung der Wurzel:
[[...slug]]passt sowohl auf die Wurzel als auch auf verschachtelte Pfade. An der Wurzel istparams.sluggleichundefined. Normalisieren Sie es zu[].[...slug]passt nie auf die Wurzel. Mindestens ein Segment ist erforderlich. - Typisierung: Typisieren Sie immer explizit:
params: { slug?: string[] }. Das verhindert Fehler beim Zugriff aufparams.slug.length. - Statische Pfade: Fügen Sie
{ slug: [] }hinzu, wenn Sie die Wurzel vorab bauen wollen. Mit dynamicParams = false liefert jeder Pfad, den generateStaticParams nicht zurückgibt, eine 404. Links: Verketten Sie Segmente nie direkt. Verwenden Sie beim Erzeugen von hrefsencodeURIComponent. - Routenkonflikte: Route Groups (z. B.
(marketing)) erscheinen nicht in der URL, beeinflussen aber die Layout-Hierarchie. Halten Sie[[...slug]]in seinem Bereich isoliert, wenn Sie ähnliche Muster haben. 404-Seiten: Rufen Sie bei ungültigen Pfaden immernotFound()auf. Passen Sie app/not-found.tsx für Ihre UX an.
Vollständiges Beispiel eines Dokumentationsbereichs mit [[...slug]]
Struktur:
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>
);
}
not-found.tsx:
export default function NotFound() {
return <div>Page not found</div>;
}
Checkliste vor dem Release
/docsöffnet die Wurzelseite.- Tiefe Pfade wie
/docs/a/b/cwerden korrekt gerendert. params.slugwird an der Wurzel zu[]normalisiert.- Breadcrumbs erzeugen korrekte, kodierte Links.
generateMetadataliefert für Wurzel und verschachtelte Seiten den richtigen Titel und das richtige Canonical.notFound()funktioniert bei ungültigen Knoten.- Die Einstellungen
revalidateunddynamicentsprechen den Anforderungen an die Aktualität.
Die wichtigsten Erkenntnisse
[[...slug]] bietet eine saubere Architektur für Bereiche, in denen die Wurzel und die verschachtelten Seiten sich eine Seite und ein Layout teilen. Es vereinfacht Navigation, Breadcrumbs, SEO und die gesamte Wartung. Die wichtigsten Punkte:
- Typisieren Sie
params.slugkorrekt und normalisieren Sie es an der Wurzel zu[]. - Bauen Sie Links sicher mit
encodeURIComponent. - Bauen Sie Pfade bei Bedarf mit
{ slug: [] }vorab. - Setzen Sie SEO mit
generateMetadataum. - Geben Sie bei ungültigen Pfaden immer
notFound()zurück.