Logboek / Ontwikkeling van webproducten

Dynamische routes instellen in Next.js. Volledige uitleg van [[...slug]]

Dynamische routes instellen in Next.js. Volledige uitleg van [[...slug]]

Dynamische routing in de App Router geeft flexibiliteit bij het bouwen van een blog, catalogus, webshop of documentatiesite. Basisgevallen kun je afhandelen met [id] en [...slug], maar voor secties met optionele nesting biedt [[...slug]] de handigste architectuur.

In dit artikel behandelen we alle drie de patronen, maar we richten ons vooral op [[...slug]]: hoe het bij URL's past, wat er in params binnenkomt, hoe je het veilig declareert, hoe je statische paden genereert, hoe je breadcrumbs bouwt en hoe je SEO afhandelt.

Snel overzicht

  • [slug] - één segment. Voorbeeld: /blog/my-post params.slug: string
  • [[...slug]] - verplichte catch-all (minstens één segment nodig). Voorbeeld: /docs/getting-started/install params.slug: string[]
  • [[...slug]] - optionele catch-all (past op zowel de root als op een willekeurig aantal segmenten). Voorbeelden: /shop, /shop/men, /shop/men/t-shirts params.slug: string[] | undefined

Wanneer je [[...slug]] gebruikt

  • Eén component en layout voor zowel de root als alle geneste niveaus van een sectie. Voorbeeld: documentatie of een catalogus waar de startpagina van de sectie en de subpagina's dezelfde structuur hebben.
  • Je hebt een rootpagina zonder segmenten nodig, plus onbeperkte nesting daaronder. Voorbeeld: /docs als inhoudsopgave en diepere paden zoals /docs/guides/setup/cloud.
  • Breadcrumbs en navigatie worden opgebouwd uit een array van segmenten, maar de rootpagina moet ook werken.

Basisimplementatie van [[...slug]]

Mapstructuur:

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>
  );
}

Belangrijkste punten:

  • Op de root is params.slug undefined. Zet het meteen om naar [].
  • De route past zowel op /docs als op elke nesting daaronder.

Breadcrumbs vanuit [[...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>
  );
}

Gebruik:

export default function DocsPage({ params }: { params: { slug?: string[] } }) {
  const segments = params.slug ?? [];
  return (
    <>
      <Breadcrumbs segments={segments} />
      {/* content */}
    </>
  );
}

Best practices:

  • Gebruik altijd encodeURIComponent bij het genereren van links.
  • Voor weergave kun je decoderen om leesbare namen te tonen.

Statische paden genereren voor [[...slug]]

Ken je sommige paden vooraf en wil je ze vooraf bouwen, gebruik dan generateStaticParams. Met een optionele catch-all kun je zowel geneste paden als de root genereren.

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
  ];
}

Opmerkingen:

  • Bij [[...slug]] is het handig om het rootpad te beschrijven als { slug: [] }. Dat maakt duidelijk dat er een pad zonder segmenten gegenereerd moet worden.
  • Om de geldige statische paden strikt te beperken, gebruik je:

SEO en canonical links voor [[...slug]]

generateMetadata ontvangt params en laat je metadata genereren op basis van de segmenten.

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 }
  };
}

Aanbevelingen:

  • Wees voorzichtig met dubbele URL's. Gebruik je trailingSlash, zorg dan dat de canonical past bij de werkelijke configuratie.
  • Stel voor pagina's op basis van data een redelijke revalidate in, zodat de metadata altijd vers is.

Controle over cache en rendering

  • Standaard statisch met ISR: export const revalidate = 300;
  • Volledig dynamisch antwoord:
await fetch(url, { cache: 'no-store' });

// of op routeniveau: export const dynamic = 'force-dynamic';

  • Gemengde aanpak: cache een deel van de data en haal ander deel live op. Overweeg voor vaak bijgewerkte blokken Route Handlers met eigen cache.

[[...slug]] vs [...slug] en veelvoorkomende valkuilen

  • Afhandeling van de root [[...slug]] past op zowel de root als geneste paden. Op de root is params.slug undefined. Normaliseer het naar []. [...slug] past nooit op de root. Er is minstens één segment nodig.
  • Typering Type altijd expliciet: params: { slug?: string[] }. Zo voorkom je fouten bij het benaderen van params.slug.length.
  • Statische paden Voeg { slug: [] } toe als je de root vooraf wilt bouwen. Met dynamicParams = false geeft elk pad dat niet door generateStaticParams wordt teruggegeven een 404. Links Plak segmenten nooit rechtstreeks aan elkaar. Gebruik encodeURIComponent bij het genereren van hrefs.
  • Routeconflicten Routegroepen (bijv. (marketing)) verschijnen niet in de URL, maar beïnvloeden de layouthiërarchie. Houd
  • [[...slug]] geïsoleerd in zijn sectie als je vergelijkbare patronen hebt. 404's Roep altijd notFound() aan voor ongeldige paden. Pas app/not-found.tsx aan voor jouw UX.
  • **Volledig voorbeeld van een documentatiesectie met **[[...slug]] layout.tsx: page.tsx:

generateStaticParams.ts :

app/
  docs/
    layout.tsx
    [[...slug]]/
      page.tsx
      loading.tsx
      not-found.tsx

not-found.tsx:

export default function DocsLayout({ children }: { children: React.ReactNode }) {
  return (
    <div className="docs">
      <aside>{/* section menu */}</aside>
      <main>{children}</main>
    </div>
  );
}

Checklist voor de release/docs

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>
  );
}

opent de rootpagina. Diepe paden zoals

/docs/a/b/c worden correct gerenderd.

export default function NotFound() {
  return <div>Page not found</div>;
}

params.slug

  • wordt op de root genormaliseerd naar[]
  • . Breadcrumbs genereren correcte, gecodeerde links. generateMetadata
  • geeft een juiste title en canonical terug voor zowel de root als geneste pagina's. notFound() werkt voor ongeldige nodes. revalidate
  • en
  • dynamic-instellingen komen overeen met de eisen aan versheid.
  • Belangrijkste conclusies [[...slug]]
  • geeft een nette architectuur voor secties waarin zowel de root als geneste pagina's één pagina en layout delen. Het vereenvoudigt navigatie, breadcrumbs, SEO en het onderhoud als geheel. De belangrijkste punten: Type params.slug

goed en normaliseer het naar

[] op de root.

Bouw links veilig met

  • encodeURIComponent . Bouw paden vooraf met { slug: [] } als dat nodig is.
  • Regel SEO met generateMetadata.
  • Geef altijd notFound() terug voor ongeldige paden.
  • Dynamische routes in Next.js: volledige uitleg van [[...slug]] Gedetailleerde gids over dynamische routes in Next.js: [slug], [...slug] en [[...slug]]. Uitleg over hoe parameters werken, het genereren van statische paden, SEO en het afhandelen van 404..
  • Geef altijd notFound() terug voor ongeldige paden.