nextjsroutingdynamicMenengah4 mnt baca

Catch-all & Optional Route

Menangkap banyak segmen: [...slug] dan [[...slug]].

Dari satu segmen ke banyak segmen

Modul sebelumnya membahas [slug] yang menangkap tepat satu segmen URL. Tapi bagaimana dengan dokumentasi yang strukturnya bertingkat, seperti /docs/panduan/instalasi/windows? Kedalamannya tidak bisa ditebak: bisa dua level, bisa lima level, tergantung seberapa dalam penulis menyusunnya.

Catch-all route menjawabnya: satu file menangkap sisa seluruh segmen URL sekaligus, berapa pun jumlahnya, lalu menyerahkannya sebagai array.

Contoh 1: [...slug] menangkap satu atau lebih segmen

Tiga titik di dalam kurung siku mengubah segmen menjadi array berisi semua segmen yang tertangkap:

app/docs/[...slug]/page.tsx → /docs/a => slug = ["a"] → /docs/a/b => slug = ["a", "b"] → /docs/a/b/c => slug = ["a", "b", "c"]

Perhatikan /docs saja (tanpa segmen tambahan) tidak cocok dan akan 404, karena catch-all biasa butuh minimal satu segmen. Contoh membacanya:

tsx
// app/docs/[...slug]/page.tsx
export default async function Docs({
  params,
}: {
  params: Promise<{ slug: string[] }>;
}) {
  const { slug } = await params;
  return (
    <div>
      <p>Kedalaman: {slug.length} level</p>
      <p>Path: {slug.join(" / ")}</p>
    </div>
  );
}

Tipenya string[] karena nilainya selalu array. Di aplikasi dokumentasi nyata, array ini biasanya dipakai untuk mencari file konten yang cocok, misalnya slug.join("/") menjadi path panduan/instalasi/windows.md di dalam CMS atau folder konten.

Contoh 2: breadcrumb otomatis dari slug

Pola paling umum untuk catch-all adalah breadcrumb, daftar link navigasi yang mengikuti kedalaman URL:

tsx
// app/docs/[...slug]/page.tsx
import Link from "next/link";

export default async function Docs({
  params,
}: {
  params: Promise<{ slug: string[] }>;
}) {
  const { slug } = await params;

  return (
    <nav>
      <Link href="/docs">Docs</Link>
      {slug.map((part, i) => (
        <span key={part}>
          {" / "}
          <Link href={"/docs/" + slug.slice(0, i + 1).join("/")}>{part}</Link>
        </span>
      ))}
    </nav>
  );
}

Untuk URL /docs/panduan/instalasi, breadcrumb yang tampil adalah Docs / panduan / instalasi, dan setiap potongannya adalah link yang bisa diklik menuju level tersebut. Satu file menghasilkan breadcrumb untuk kedalaman berapa pun, tanpa konfigurasi tambahan.

[[...slug]]: versi opsional yang boleh kosong

Tambah satu pasang kurung siku lagi dan route induk ikut tertangani oleh file yang sama:

app/shop/[[...slug]]/page.tsx → /shop => slug = undefined → /shop/promo => slug = ["promo"] → /shop/promo/a => slug = ["promo", "a"]

Karena slug bisa undefined, tipenya opsional dan kamu wajib menyediakan fallback:

tsx
// app/shop/[[...slug]]/page.tsx
export default async function Shop({
  params,
}: {
  params: Promise<{ slug?: string[] }>;
}) {
  const { slug } = await params;

  if (!slug) {
    return <h1>Semua produk</h1>;
  }
  return <h1>Kategori: {slug.join(" / ")}</h1>;
}

Pola ini cocok untuk halaman katalog: /shop menampilkan semua produk, /shop/elektronik memfilter satu kategori, /shop/elektronik/hp memfilter lebih dalam, semuanya dilayani satu file yang sama.

Catatan: pakai catch-all untuk konten bertingkat yang memang tidak bisa ditebak kedalamannya (dokumentasi, CMS, kategori bertingkat). Jangan pakai untuk route biasa yang strukturnya tetap dan diketahui, karena URL menjadi sulit ditebak, susah diingat pengguna, dan lemah untuk SEO.

Tantangan

Dokumentasi bertingkat

Buat app/docs/[...slug]/page.tsx yang me-render breadcrumb dari array slug (mis. /docs/panduan/instalasi → Panduan / Instalasi).