typescriptintermediate

Catch-All and Optional Catch-All Routes

Handle dynamic nested routes with catch-all [...slug] and optional [[...slug]] segments.

typescript
// app/docs/[[...slug]]/page.tsx
import { notFound } from 'next/navigation';

interface DocPage {
  title: string;
  content: string;
}

async function getDoc(slugParts: string[]): Promise<DocPage | null> {
  const path = slugParts.join('/');
  const res = await fetch(`https://api.example.com/docs/${path || 'index'}`);
  if (!res.ok) return null;
  return res.json();
}

export async function generateStaticParams() {
  const res = await fetch('https://api.example.com/docs/all-paths');
  const paths: string[][] = await res.json();
  return [
    { slug: [] },
    ...paths.map((segments) => ({ slug: segments })),
  ];
}

export const dynamicParams = false;

export default async function DocsPage({
  params,
}: {
  params: Promise<{ slug?: string[] }>;
}) {
  const { slug = [] } = await params;
  const doc = await getDoc(slug);
  if (!doc) notFound();

  return (
    <article className="prose dark:prose-invert">
      <h1>{doc.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: doc.content }} />
    </article>
  );
}

Use Cases

  • documentation sites
  • nested content pages
  • CMS-driven routes

Tags

Related Snippets

Similar patterns you can reuse in the same workflow.