Retour au cours

frontend / nextjs

Routes dynamiques

Leçon 31 exercice

Explication

Ce que vous allez apprendre

  • Créer un segment de route dynamique avec [slug] et récupérer sa valeur
  • Capturer plusieurs segments d'un coup avec [...categories] (catch-all)
  • Distinguer un catch-all obligatoire ([...slug]) d'un catch-all optionnel ([[...slug]])
  • Lire params correctement (une Promise à await depuis Next.js 15)
  • Pré-générer des pages au build avec generateStaticParams
  • Afficher une vraie page 404 avec notFound() plutôt qu'un plantage

Dans quel contexte ?

Une boutique en ligne héberge un catalogue de plusieurs milliers de produits, chacun avec sa propre fiche à l'URL /produits/casque-audio-sans-fil. Créer un fichier page.tsx par produit serait absurde et impossible à maintenir. L'équipe a aussi besoin qu'une URL invalide comme /produits/produit-supprime-depuis-longtemps affiche une vraie page "Produit introuvable" avec un lien vers le catalogue, plutôt qu'une erreur serveur brute. C'est exactement ce que les routes dynamiques et notFound() permettent de faire avec un seul fichier.

Le problème, d'abord

Imagine un blog avec des milliers d'articles. Créer un dossier par article (app/blog/mon-premier-article/page.tsx, app/blog/mon-deuxieme-article/page.tsx, etc.) serait absurde à maintenir.

Les routes dynamiques résolvent exactement ça. Un seul fichier capture n'importe quelle valeur présente dans l'URL et l'utilise pour aller chercher le bon contenu.

Voyons la syntaxe, la plus simple d'abord. Des crochets dans le nom du dossier créent un segment dynamique : [slug] capture exactement UN segment d'URL, par exemple /blog/mon-article donne slug = "mon-article".

Et si on a besoin de capturer plusieurs segments ? [...categories] (catch-all) capture PLUSIEURS segments d'un coup, sous forme de tableau : /shop/tech/laptops devient ["tech", "laptops"].

Il existe encore une variante plus souple. [[...path]] (catch-all optionnel) fait la même chose, mais accepte aussi qu'il n'y ait AUCUN segment du tout.

SyntaxeExemple d'URL capturéeRésultat
[slug]/blog/mon-articleslug = "mon-article"
[...categories]/shop/tech/laptopscategories = ["tech", "laptops"] (au moins 1 segment requis)
[[...path]]/docs ou /docs/a/bpath = undefined ou ["a", "b"] (0 segment accepté)

Prérequis

Il faut être à l'aise avec les layouts imbriqués et la notion de segment de route vus dans la leçon précédente pour bien comprendre comment un segment dynamique s'insère dans l'arborescence.

Comment récupérer ces valeurs dans le code ? Elles arrivent via la prop params. Un détail à bien noter : depuis Next.js 15, params est une Promise qu'il faut awaiter avant de l'utiliser — une source d'erreur fréquente pour qui a appris sur une version plus ancienne.

Une fois qu'on sait capturer une valeur, une question se pose : faut-il générer la page à chaque requête, ou à l'avance ? Par défaut, une route dynamique se rend à la demande, requête par requête.

Mais on peut aussi précalculer certaines pages au moment du build. generateStaticParams indique à Next.js la liste des valeurs à pré-générer, comme la liste des slugs d'articles existants — des pages ultra-rapides, sans sacrifier la flexibilité.

Les pièges courants à connaître avant de pratiquer :

  • Oublier le await sur params (l'erreur classique post-Next.js 15).
  • Ne pas gérer le cas où la donnée n'existe pas : notFound() sert justement à afficher proprement une page 404 plutôt qu'un plantage.
  • Confondre [...slug] (au moins un segment obligatoire) et [[...slug]] (zéro ou plusieurs segments acceptés).

Piège fréquent

Oublier await sur params depuis Next.js 15 (const { slug } = params au lieu de const { slug } = await params) provoque une erreur TypeScript ou un comportement incorrect au runtime. C'est l'erreur la plus signalée par les développeurs venant d'une version antérieure à Next.js 15.

Et la suite ? Cette leçon prolonge directement la précédente : on savait organiser des routes fixes avec des layouts, on sait maintenant les rendre paramétrables.

Commandes & code

Routes dynamiques

text
app/
├── blog/
│   └── [slug]/
│       └── page.tsx          # /blog/mon-article
├── shop/
│   └── [...categories]/
│       └── page.tsx          # /shop/tech/laptops/gaming (catch-all)
└── docs/
    └── [[...path]]/
        └── page.tsx          # /docs OU /docs/a/b (catch-all optionnel)
tsx
// app/blog/[slug]/page.tsx
interface PageProps {
  params: Promise<{ slug: string }>;
}

export default async function BlogPostPage({ params }: PageProps) {
  const { slug } = await params; // params est une Promise depuis Next.js 15
  const post = await getPostBySlug(slug);

  if (!post) {
    // déclenche app/blog/[slug]/not-found.tsx
    notFound();
  }

  return (
    <article>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.html }} />
    </article>
  );
}

import { notFound } from "next/navigation";
async function getPostBySlug(slug: string) {
  const res = await fetch(`https://api.example.com/posts/${slug}`);
  if (res.status === 404) return null;
  return res.json();
}
tsx
// app/shop/[...categories]/page.tsx — catch-all : capture N segments dans un tableau
interface PageProps {
  params: Promise<{ categories: string[] }>;
}

export default async function ShopPage({ params }: PageProps) {
  const { categories } = await params;
  // /shop/tech/laptops/gaming -> categories = ["tech", "laptops", "gaming"]
  const products = await getProductsByPath(categories);

  return (
    <div>
      <Breadcrumb path={categories} />
      <ProductGrid products={products} />
    </div>
  );
}
tsx
// generateStaticParams : pré-génère les pages statiques au build (SSG)
export async function generateStaticParams() {
  const posts = await fetch("https://api.example.com/posts").then((r) => r.json());

  return posts.map((post: { slug: string }) => ({
    slug: post.slug,
  }));
}

// Contrôle le comportement pour les slugs NON générés au build
export const dynamicParams = true; // false -> 404 pour tout slug hors de la liste
tsx
// app/blog/[slug]/generateMetadata — métadonnées dynamiques par route
import type { Metadata } from "next";

export async function generateMetadata({
  params,
}: {
  params: Promise<{ slug: string }>;
}): Promise<Metadata> {
  const { slug } = await params;
  const post = await getPostBySlug(slug);

  return {
    title: post?.title ?? "Article introuvable",
    openGraph: { images: [post?.coverImage] },
  };
}

Résumé

  • [slug] capture un segment, [...slug] en capture plusieurs, [[...slug]] les rend optionnels.
  • params est une Promise à await depuis Next.js 15 (Server Components et generateMetadata).
  • generateStaticParams pré-rend les pages au build pour du SSG performant.
  • notFound() déclenche le fichier not-found.tsx le plus proche.

Exercices pratiques

1 disponible
1

Mission : le catalogue qui plante en production

Objectif : Corriger une route dynamique qui n'attend pas params et ne gère pas les produits introuvables.

Contexte

Une boutique vient de migrer vers Next.js 15. Depuis, la page app/produits/[slug]/page.tsx renvoie une erreur TypeScript en production : const { slug } = params; sans await. De plus, quand un produit n'existe plus, l'utilisateur voit une page blanche au lieu d'un message clair.

Résoudre l’exercice →