Dziennik / Rozwój produktów webowych

Jak skonfigurować dynamiczne trasy w Next.js. Pełne omówienie [[...slug]]

Jak skonfigurować dynamiczne trasy w Next.js. Pełne omówienie [[...slug]]

Dynamiczne routing w Routerze aplikacji zapewniają elastyczność przy budowie bloga, katalogu, sklepu lub witryny dokumentacyjnej. Podstawowe przypadki użycia można obsługiwać za pomocą [id] [...slug], ale w przypadku sekcji z opcjonalnym zagnieżdżaniem najbardziej wygodną architekturę zapewnia [[...slug]].

W tym artykule omówimy wszystkie trzy wzorce, ale skupimy się głównie na [[...slug]]: jak pasuje do adresów URL, co przychodzi w params, jak bezpiecznie zadeklarować go, jak generować statyczne ścieżki, jak budować breadcrumby i jak obsługiwać SEO.

Szybka Mapa

  • [slug] - jeden segment Przykład: /blog/my-post params.slug: string
  • [[...slug]] - wymagany catch-all (potrzebny jest przynajmniej jeden segment) Przykład: /docs/getting-started/install params.slug: string[]
  • [[...slug]] - opcjonalny catch-all (pasuje zarówno do roota, jak i dowolnej liczby segmentów) Przykłady: /shop, /shop/men, /shop/men/t-shirts params.slug: string[] | undefined

Kiedy użyć [[...slug]]

  • Jeden komponent i układ dla zarówno roota, jak i wszystkich zagnieżdżonych poziomów sekcji. Przykład: dokumentacja lub katalog, gdzie strona główna sekcji i podstrony mają taką samą strukturę.
  • Potrzebujesz strony root bez segmentów, plus nielimitowanego zagnieżdżania poniżej. Przykład: /docs jako spis treści i głębsze ścieżki, takie jak /docs/guides/setup/cloud.
  • Breadcrumby i nawigacja są budowane z tablicy segmentów, ale strona root również musi działać.

Podstawowa Implementacja [[...slug]]

Struktura katalogu:

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

Główne punkty:

  • Na root, params.slug będzie undefined. Natychmiast przekonwertuj go na [].
  • Trasa pasuje zarówno do /docs jak i do dowolnych zagnieżdżeń.

Breadcrumby od [[...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} />
      {/* content */}
    </>
  );
}

Najlepsze praktyki:

  • Zawsze używaj encodeURIComponent podczas generowania linków.
  • Do wyświetlania możesz odkodować, aby pokazać ludzie-czytelne nazwy.

Generowanie Statycznych Ścieżek dla [[...slug]]

Jeśli znasz jakieś ścieżki z góry i chcesz je zbudować z wyprzedzeniem, użyj generateStaticParams. Z opcjonalnym catch-all, możesz generować zarówno zagnieżdżone ścieżki, jak i root.

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

Uwagi:

  • Dla [[...slug]] wygodne jest opisanie rootowej ścieżki jako { slug: [] }. To jasno wskazuje, że trzeba wygenerować ścieżkę bez segmentów.
  • Aby ściśle ograniczyć prawidłowe statyczne ścieżki, użyj:

SEO i Linki Kanoniczne dla [[...slug]]

generateMetadata otrzymuje params i pozwala generować 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: '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 }
  };
}

Rekomendacje:

  • Bądź ostrożny z duplikatami adresów URL. Jeśli używasz trailingSlash upewnij się, że kanoniczne pasuje do faktycznej konfiguracji.
  • Dla stron opartych na danych, ustaw rozsądny revalidate aby metadane były zawsze świeże.

Kontrola Cache i Renderowania

  • Statyczne domyślnie z ISR:export const revalidate = 300;
  • W pełni dynamiczna odpowiedź:
await fetch(url, { cache: 'no-store' });

// lub na poziomie routingu: export const dynamic = 'force-dynamic';

  • Podejście mieszane: cacheuj niektóre dane, pobieraj inne dane na bieżąco. Dla często aktualizowanych bloków rozważ Uchwyty Tras z własnym cache.

[[...slug]] vs [...slug] and Common Pitfalls

  • Root handling [[...slug]] matches both the root and nested paths. At the root, params.slug is undefined. Normalize it to []. [...slug] never matches the root. At least one segment is required.
  • Typing Always type explicitly: params: { slug?: string[] }. This prevents errors when accessing params.slug.length.
  • Static paths Add { slug: [] } if you want to prebuild the root. With dynamicParams = false, any path not returned by generateStaticParams will give a 404. Links Never concatenate segments directly. Use encodeURIComponent when generating hrefs.
  • Route conflicts Route groups (e.g. (marketing)) don’t appear in the URL but affect layout hierarchy. Keep
  • [[...slug]] isolated in its section if you have similar patterns. 404s Always call notFound() for invalid paths. Customize app/not-found.tsx for your UX.
  • **Complete Example of a Documentation Section with **[[...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>
  );
}

Pre-Release Checklist/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>
  );
}

opens the root page. Deep paths like

/docs/a/b/c render correctly.

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

params.slug

  • is normalized to[]
  • at the root. Breadcrumbs generate correct, encoded links. generateMetadata
  • returns proper title and canonical for both root and nested pages. notFound() works for invalid nodes. revalidate
  • and
  • dynamic settings match freshness requirements.
  • Key Takeaways [[...slug]]
  • provides a clean architecture for sections where both the root and nested pages share a single page and layout. It simplifies navigation, breadcrumbs, SEO, and overall maintenance. The most important points: Type params.slug

properly and normalize it to

[] at the root.

Build links safely with

  • encodeURIComponent . Prebuild paths with { slug: [] } if needed.
  • Handle SEO with generateMetadata.
  • Always return notFound() for invalid paths.
  • Dynamic routes in Next.js: full breakdown of [[...slug]] Detailed guide on dynamic routes in Next.js: [slug], [...slug] and [[...slug]]. Explaining how parameters work, generating static paths, SEO, and handling 404..
  • Always return notFound() for invalid paths.