frontend / nextjs
Routes dynamiques
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
paramscorrectement (unePromiseàawaitdepuis 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.
| Syntaxe | Exemple d'URL capturée | Résultat |
|---|---|---|
[slug] | /blog/mon-article | slug = "mon-article" |
[...categories] | /shop/tech/laptops | categories = ["tech", "laptops"] (au moins 1 segment requis) |
[[...path]] | /docs ou /docs/a/b | path = 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
awaitsurparams(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
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)// 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();
}// 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>
);
}// 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// 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.paramsest unePromiseàawaitdepuis Next.js 15 (Server Components etgenerateMetadata).generateStaticParamspré-rend les pages au build pour du SSG performant.notFound()déclenche le fichiernot-found.tsxle plus proche.
Exercices pratiques
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.