Next.js : routage avec l'App Router
L’App Router associe chaque dossier de app/ à un segment d’URL. Un dossier est une route publique seulement s’il contient un fichier page.
Fichiers spéciaux
Section intitulée « Fichiers spéciaux »| Fichier | Rôle |
|---|---|
page.tsx |
Interface de la route (la rend publique) |
layout.tsx |
Gabarit partagé par la route et ses enfants (conservé à la navigation) |
loading.tsx |
Interface de chargement (affichée pendant le chargement de la page) |
error.tsx |
Interface d’erreur (composant client) |
not-found.tsx |
Page 404 de la route |
route.ts |
Route d’API (voir Données et routes API) |
template.tsx |
Comme layout, mais recréé à chaque navigation |
app/├── layout.tsx├── page.tsx # /├── a-propos/│ └── page.tsx # /a-propos├── blog/│ ├── layout.tsx # gabarit du blog│ ├── page.tsx # /blog│ └── [slug]/│ ├── page.tsx # /blog/mon-article│ └── loading.tsx└── not-found.tsxLayouts imbriqués
Section intitulée « Layouts imbriqués »app/blog/layout.tsx :
export default function BlogLayout({ children,}: { children: React.ReactNode;}) { return ( <section> <nav>Navigation du blog</nav> {children} </section> );}Le layout du blog est inséré dans le layout racine. Un layout ne se re-rend pas quand on navigue entre ses pages enfants.
Routes dynamiques
Section intitulée « Routes dynamiques »app/blog/[slug]/page.tsx :
type Props = { params: Promise<{ slug: string }> };
export default async function Article({ params }: Props) { const { slug } = await params; return <h1>Article : {slug}</h1>;}| Dossier | Correspond à |
|---|---|
[slug] |
Un segment (/blog/a) |
[...chemin] |
Un ou plusieurs segments (/docs/a/b) |
[[...chemin]] |
Zéro, un ou plusieurs segments |
Depuis Next.js 15, params est une promesse : l’attendre avec await.
Pages statiques à la construction
Section intitulée « Pages statiques à la construction »export async function generateStaticParams() { const articles = await listerArticles(); return articles.map((a) => ({ slug: a.slug }));}
export const dynamicParams = false; // 404 pour un slug absent de la listeMétadonnées dynamiques
Section intitulée « Métadonnées dynamiques »import type { Metadata } from 'next';
export async function generateMetadata({ params }: Props): Promise<Metadata> { const { slug } = await params; const article = await trouverArticle(slug); return { title: article.titre, description: article.resume };}Paramètres de recherche
Section intitulée « Paramètres de recherche »type Props = { searchParams: Promise<{ page?: string; q?: string }> };
export default async function Recherche({ searchParams }: Props) { const { page = '1', q = '' } = await searchParams; return ( <p> Recherche « {q} », page {page} </p> );}Lire searchParams rend la page dynamique (rendue à chaque requête).
Liens et navigation
Section intitulée « Liens et navigation »import Link from 'next/link';
<Link href="/blog">Blog</Link>;<Link href={`/blog/${slug}`} prefetch={false}> Lire</Link>;Link navigue sans recharger la page et précharge les routes visibles. Dans un composant client :
'use client';import { usePathname, useRouter, useSearchParams } from 'next/navigation';
export function Menu() { const chemin = usePathname(); const routeur = useRouter(); const parametres = useSearchParams(); return ( <button onClick={() => routeur.push('/blog?page=2')}> Page suivante (actuelle : {chemin}, {parametres.get('page') ?? '1'}) </button> );}Côté serveur : redirect('/connexion') et notFound() (importés de next/navigation).
import { notFound, redirect } from 'next/navigation';
export default async function Page({ params }: Props) { const { slug } = await params; const article = await trouverArticle(slug); if (!article) notFound(); if (article.deplace) redirect(`/blog/${article.nouveauSlug}`); return <h1>{article.titre}</h1>;}Groupes de routes et dossiers privés
Section intitulée « Groupes de routes et dossiers privés »| Dossier | Effet |
|---|---|
(site) |
Groupe : organise les fichiers sans influencer l’URL, permet des layouts différents |
_composants |
Dossier privé : exclu du routage |
app/├── (site)/layout.tsx # layout public├── (site)/page.tsx # /└── (admin)/admin/page.tsx # /admin, layout séparéPages d’erreur et 404
Section intitulée « Pages d’erreur et 404 »app/error.tsx (composant client obligatoire) :
'use client';
export default function Erreur({ error, reset,}: { error: Error & { digest?: string }; reset: () => void;}) { return ( <div role="alert"> <h2>Une erreur est survenue</h2> <button onClick={reset}>Réessayer</button> </div> );}app/not-found.tsx :
import Link from 'next/link';
export default function NonTrouve() { return ( <main> <h1>Page introuvable</h1> <Link href="/">Retour à l'accueil</Link> </main> );}Next.js répond 404 pour les URL inconnues avec cette page.
Redirections et réécritures
Section intitulée « Redirections et réécritures »next.config.ts :
const config: NextConfig = { async redirects() { return [{ source: '/ancien', destination: '/nouveau', permanent: true }]; }, async rewrites() { return [ { source: '/api/v1/:path*', destination: 'http://127.0.0.1:3001/:path*' }, ]; },};Dépannage
Section intitulée « Dépannage »| Symptôme | Piste |
|---|---|
| Un dossier renvoie 404 | page.tsx manquant dans le dossier |
params.slug est une promesse |
Depuis Next.js 15 : const { slug } = await params |
useRouter : invariant expected app router to be mounted |
Importer depuis next/navigation (et non next/router) |
You're importing a component that needs useState |
Ajouter 'use client' en tête du fichier |
| Le layout ne se met pas à jour à la navigation | Comportement voulu : utiliser template.tsx ou un composant client |
error.tsx non pris en compte |
Il doit être un composant client ('use client') |
Ubuntu 24.04 et 26.04 : différences à connaître
Section intitulée « Ubuntu 24.04 et 26.04 : différences à connaître »Aucune.
Pour aller plus loin
Section intitulée « Pour aller plus loin »- Routage
- Suite : Composants serveur et client