Aller au contenu

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.

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.tsx

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.

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.

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 liste
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 };
}
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).

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>;
}
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é

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.

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*' },
];
},
};
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.