Cours Next.js · Module 2: Routing et layouts

Gestion des 404 et des redirections (not-found.js et redirects)

14 min de lecture
Dans cette leçon6

Dans la vaste ville quantique de Metropolis Quantum, la navigation devient un élément clé de la vie quotidienne. Mais que se passe-t-il lorsqu'un habitant ou un touriste essaie d'atteindre un lieu qui n'existe pas ? Le système de la ville doit gérer un tel cas avec élégance - soit en informant que le lieu n'existe pas, soit en redirigeant vers un emplacement alternatif.

De la même manière, dans les applications web construites avec Next.js 16, nous devons gérer élégamment les situations où un utilisateur tente d'accéder à une page ou une ressource inexistante. Ce chapitre se concentre sur la création de pages 404 personnalisées et l'implémentation des redirections dans Next.js 16.

Gestion des pages 404 (Not Found)

Dans Next.js 16, on peut gérer les pages 404 de deux manières principales :

  1. Une page 404 globale pour toute l'application
  2. Des pages 404 personnalisées pour des segments ou des paramètres dynamiques précis

Page 404 globale

Pour créer une page 404 globale dans Next.js 16, tu dois créer un fichier not-found.js ou not-found.tsx dans le répertoire racine app :

1// app/not-found.tsx
2import Link from 'next/link';
3
4export default function NotFound() {
5  return (
6    <div className="quantum-not-found">
7      <h1>404 : Emplacement quantique introuvable</h1>
8      <div className="quantum-hologram">
9        <div className="hologram-effect">404</div>
10      </div>
11      <p>
12        Nous sommes désolés, mais le lieu que tu recherches n'existe pas à Metropolis Quantum.
13        Nos capteurs quantiques n'ont pas pu localiser la destination demandée dans aucune des
14        dimensions spatio-temporelles disponibles.
15      </p>
16      <p>
17        Causes possibles de l'erreur :
18      </p>
19      <ul>
20        <li>Les coordonnées quantiques sont incorrectes</li>
21        <li>Le lieu existait mais a été supprimé</li>
22        <li>Des fluctuations quantiques ont temporairement perturbé l'accès</li>
23      </ul>
24      <div className="action-buttons">
25        <Link href="/" className="quantum-button primary">
26          Retour au centre de contrôle
27        </Link>
28        <Link href="/map" className="quantum-button secondary">
29          Ouvrir la carte quantique
30        </Link>
31      </div>
32    </div>
33  );
34}

Cette page s'affichera automatiquement lorsqu'un utilisateur tentera d'accéder à un chemin d'URL inexistant.

Pages 404 personnalisées pour des segments précis

Next.js 16 permet aussi de créer des pages 404 personnalisées pour des segments précis de l'application. Tu peux créer un fichier not-found.js ou not-found.tsx dans n'importe quel répertoire, et il sera utilisé pour ce segment précis et ses sous-chemins :

1// app/quantum-properties/not-found.tsx
2import Link from 'next/link';
3
4export default function QuantumPropertiesNotFound() {
5  return (
6    <div className="property-not-found">
7      <h1>Propriété quantique introuvable</h1>
8      <p>
9        Malheureusement, nous n'avons pas pu trouver la propriété quantique que tu recherches.
10        Nos capteurs n'ont détecté aucune signature quantique à l'emplacement indiqué.
11      </p>
12      <div className="property-suggestions">
13        <h2>Découvre nos propriétés les plus populaires :</h2>
14        <div className="suggestion-list">
15          {/* Liste des propriétés populaires */}
16        </div>
17      </div>
18      <Link href="/quantum-properties" className="quantum-button">
19        Retour à la recherche de propriétés
20      </Link>
21    </div>
22  );
23}

Déclencher not-found depuis un composant

On peut aussi déclencher par programmation l'affichage d'une page 404 depuis n'importe quel composant ou fonction de chargement de données, en utilisant la fonction notFound de next/navigation :

1// app/quantum-properties/[id]/page.tsx
2import { notFound } from 'next/navigation';
3import { getPropertyById } from '@/lib/api';
4
5export default async function QuantumPropertyPage({ params }: { params: Promise<{ id: string }> }) {
6  const property = await getPropertyById((await params).id);
7
8  // Si la propriété n'existe pas, on affiche la page 404
9  if (!property) {
10    notFound();
11  }
12
13  return (
14    <div className="quantum-property-details">
15      <h1>{property.name}</h1>
16      {/* Détails de la propriété */}
17    </div>
18  );
19}

C'est particulièrement utile pour les pages dynamiques où l'on veut vérifier si une ressource donnée existe, et si ce n'est pas le cas - afficher la page 404.

Générer les métadonnées pour la page 404

On peut aussi personnaliser les métadonnées de notre page 404 pour améliorer le SEO et l'expérience utilisateur :

1// app/not-found.tsx
2import type { Metadata } from 'next';
3import Link from 'next/link';
4
5export const metadata: Metadata = {
6  title: 'Page introuvable | Metropolis Quantum',
7  description: 'Nous n\'avons pas pu trouver la page que tu recherches à Metropolis Quantum.',
8};
9
10export default function NotFound() {
11  // Contenu de la page 404
12}

Redirections dans Next.js 16

À Metropolis Quantum, lorsqu'un ancien lieu est déplacé ou remplacé, le système de transport redirige automatiquement les voyageurs vers la nouvelle destination. Dans Next.js, on utilise de la même manière les redirections pour diriger les utilisateurs depuis des URL obsolètes vers de nouveaux emplacements.

Next.js 16 propose plusieurs méthodes pour implémenter des redirections :

  1. Redirections statiques dans la configuration
  2. Redirections dynamiques dans le code
  3. Redirections temporaires vs permanentes

Redirections statiques dans next.config.js

La façon la plus simple de définir des redirections est d'utiliser le fichier de configuration next.config.js :

1// next.config.js
2module.exports = {
3  async redirects() {
4    return [
5      {
6        // Redirection de l'ancien chemin vers le nouveau
7        source: '/old-quantum-district',
8        destination: '/quantum-districts/central',
9        permanent: true, // code de statut 308 (redirection permanente)
10      },
11      {
12        // Redirection avec paramètres
13        source: '/quantum-labs/:labId',
14        destination: '/research-facilities/:labId',
15        permanent: false, // code de statut 307 (redirection temporaire)
16      },
17      {
18        // Redirection en conservant les paramètres de requête
19        source: '/search-old',
20        destination: '/search-new',
21        permanent: false,
22      },
23      {
24        // Redirection avec plusieurs paramètres et regex
25        source: '/quantum-zones/:zoneId(\\d{1,})',
26        destination: '/zones/:zoneId',
27        permanent: true,
28      },
29    ];
30  },
31};

Chaque redirection contient :

  • source : le chemin d'URL source
  • destination : le chemin d'URL de destination
  • permanent : un indicateur précisant si la redirection est permanente ou temporaire

La différence entre les redirections permanentes et temporaires :

  • Permanente (code 308) : informe les navigateurs et les robots des moteurs de recherche que la page a été déplacée de façon permanente. Les moteurs de recherche mémorisent cette redirection et se dirigent automatiquement vers la nouvelle URL à l'avenir.
  • Temporaire (code 307) : indique que la page est temporairement disponible à une autre adresse mais peut revenir à l'URL d'origine. Les moteurs de recherche ne mémorisent pas cette redirection longtemps.

Redirections programmatiques dans le code

Next.js 16 permet aussi des redirections dynamiques directement dans les composants ou les fonctions de données à l'aide de la fonction redirect de next/navigation :

1// app/quantum-profile/[userId]/page.tsx
2import { redirect } from 'next/navigation';
3import { getUserById } from '@/lib/api';
4
5export default async function UserProfilePage({ params }: { params: Promise<{ userId: string }> }) {
6  const user = await getUserById((await params).userId);
7
8  // Si l'utilisateur n'existe pas, on redirige vers la page de connexion
9  if (!user) {
10    redirect('/login');
11  }
12
13  // Si l'utilisateur est un admin, on redirige vers le tableau de bord admin
14  if (user.role === 'admin') {
15    redirect('/admin/dashboard');
16  }
17
18  return (
19    <div className="user-profile">
20      <h1>Profil : {user.name}</h1>
21      {/* Contenu du profil */}
22    </div>
23  );
24}

La fonction redirect est particulièrement utile dans les situations où la décision de redirection dépend de données dynamiques, comme le statut de l'utilisateur, ses permissions ou la disponibilité d'une ressource.

Redirections dans le proxy (anciennement middleware)

Le proxy (middleware dans Next.js 15 et les versions précédentes) permet d'intercepter les requêtes avant qu'elles ne soient traitées par l'application, ce qui rend possible l'implémentation d'une logique de redirection plus complexe :

1// proxy.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4
5export function proxy(request: NextRequest) {
6  // Récupère le chemin depuis l'URL
7  const pathname = request.nextUrl.pathname;
8
9  // Redirection conditionnelle selon le domaine
10  if (request.headers.get('host')?.includes('old-domain.com')) {
11    return NextResponse.redirect(
12      new URL(pathname, 'https://new-domain.com')
13    );
14  }
15
16  // Redirection des anciens chemins vers les nouveaux
17  if (pathname.startsWith('/legacy-api')) {
18    return NextResponse.redirect(
19      new URL(pathname.replace('/legacy-api', '/api/v2'), request.url)
20    );
21  }
22
23  // Redirection selon la géolocalisation
24  // NextRequest n'a plus de champ geo (supprimé dans Next.js 15) : la plateforme fournit le pays, par ex. Vercel dans l'en-tête x-vercel-ip-country
25  const country = request.headers.get('x-vercel-ip-country') || 'US';
26  if (pathname === '/events' && country === 'PL') {
27    return NextResponse.redirect(new URL('/wydarzenia', request.url));
28  }
29
30  // Redirection selon les préférences de langue du navigateur
31  const preferredLocale = request.headers.get('accept-language')?.split(',')[0].split('-')[0];
32  if (pathname === '/' && preferredLocale === 'pl' && !pathname.includes('/pl')) {
33    return NextResponse.redirect(new URL('/pl', request.url));
34  }
35
36  return NextResponse.next();
37}
38
39export const config = {
40  matcher: [
41    '/((?!api|_next/static|_next/image|favicon.ico).*)',
42  ],
43};

Le middleware nous donne une flexibilité énorme, permettant des redirections basées sur :

  • Les en-têtes HTTP
  • Les cookies
  • La géolocalisation
  • L'User-Agent
  • Les paramètres de requête
  • Et bien d'autres facteurs

Redirections conditionnelles dans les Server Components

Dans les Server Components, on peut utiliser des redirections conditionnelles basées sur des données récupérées depuis l'API ou la base de données :

1// app/quantum-experiment/[experimentId]/page.tsx
2import { redirect } from 'next/navigation';
3import { getExperimentById, getUserPermissions } from '@/lib/api';
4
5export default async function ExperimentPage({ params }: { params: Promise<{ experimentId: string }> }) {
6  // Récupère les données de l'expérience
7  const experiment = await getExperimentById((await params).experimentId);
8
9  // Si l'expérience a été déplacée, on redirige vers le nouvel emplacement
10  if (experiment?.movedTo) {
11    redirect(`/quantum-experiment/${experiment.movedTo}`);
12  }
13
14  // Si l'expérience est privée, on vérifie les permissions de l'utilisateur
15  if (experiment?.isPrivate) {
16    const userPermissions = await getUserPermissions();
17
18    if (!userPermissions.canAccessPrivateExperiments) {
19      redirect('/access-denied');
20    }
21  }
22
23  return (
24    <div className="experiment-details">
25      <h1>{experiment.title}</h1>
26      {/* Détails de l'expérience */}
27    </div>
28  );
29}

Redirections dans les Client Components

Dans les composants clients, on peut aussi effectuer des redirections à l'aide du hook useRouter :

1'use client';
2
3import { useRouter } from 'next/navigation';
4import { useEffect } from 'react';
5import { useAuth } from '@/lib/auth';
6
7export default function ProtectedPage() {
8  const router = useRouter();
9  const { user, loading } = useAuth();
10
11  useEffect(() => {
12    // Redirige les utilisateurs non authentifiés vers la page de connexion
13    if (!loading && !user) {
14      router.push('/login');
15    }
16  }, [user, loading, router]);
17
18  if (loading) {
19    return <div>Chargement...</div>;
20  }
21
22  if (!user) {
23    return null; // Ne rien afficher pendant la redirection
24  }
25
26  return (
27    <div className="protected-content">
28      <h1>Bienvenue, {user.name} !</h1>
29      {/* Contenu de la page protégée */}
30    </div>
31  );
32}

Techniques avancées de gestion des 404 et des redirections

Gérer les paramètres dynamiques inexistants

Pour les routes dynamiques avec paramètres, on peut générer des pages statiques uniquement pour certains paramètres, et afficher une page 404 pour le reste :

1// app/quantum-experiments/[experimentId]/page.tsx
2import { notFound } from 'next/navigation';
3import { getExperimentById, getAllExperimentIds } from '@/lib/api';
4
5// Génère des pages statiques pour les expériences connues
6export async function generateStaticParams() {
7  const experimentIds = await getAllExperimentIds();
8
9  return experimentIds.map((id) => ({
10    experimentId: id,
11  }));
12}
13
14export default async function ExperimentPage({ params }: { params: Promise<{ experimentId: string }> }) {
15  const experiment = await getExperimentById((await params).experimentId);
16
17  // Si l'expérience n'existe pas, on affiche la 404
18  if (!experiment) {
19    notFound();
20  }
21
22  return (
23    <div className="experiment-details">
24      <h1>{experiment.title}</h1>
25      {/* Détails de l'expérience */}
26    </div>
27  );
28}

Gérer le contenu expiré

Parfois, un contenu peut n'être disponible que temporairement ou expirer après un certain temps. On peut gérer de tels cas en redirigeant automatiquement vers des pages alternatives :

1// app/quantum-events/[eventId]/page.tsx
2import { redirect, notFound } from 'next/navigation';
3import { getEventById } from '@/lib/api';
4
5export default async function EventPage({ params }: { params: Promise<{ eventId: string }> }) {
6  const event = await getEventById((await params).eventId);
7
8  // Si l'événement n'existe pas, on affiche la 404
9  if (!event) {
10    notFound();
11  }
12
13  // Vérifie si l'événement est déjà terminé
14  const now = new Date();
15  const eventEndDate = new Date(event.endDate);
16
17  if (now > eventEndDate) {
18    // Si des enregistrements sont disponibles, on redirige vers eux
19    if (event.recordings) {
20      redirect(`/quantum-events/recordings/${(await params).eventId}`);
21    }
22
23    // S'il y a de futurs événements prévus dans cette série, on redirige vers eux
24    if (event.series && event.nextInSeries) {
25      redirect(`/quantum-events/${event.nextInSeries}`);
26    }
27
28    // Sinon, on redirige vers la page d'archives
29    redirect('/quantum-events/archive');
30  }
31
32  return (
33    <div className="event-details">
34      <h1>{event.title}</h1>
35      {/* Détails de l'événement */}
36    </div>
37  );
38}

Redirections personnalisées basées sur des tests A/B

On peut aussi implémenter des redirections basées sur des tests A/B pour diriger les utilisateurs vers différentes versions d'une page :

1// proxy.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4
5// Fonction utilitaire pour choisir la variante du test A/B
6function getABTestVariant(userId: string, testName: string, variants: string[]): string {
7  // On utilise un algorithme déterministe basé sur userId et le nom du test
8  const hash = hashString(`${userId}-${testName}`);
9  const variantIndex = hash % variants.length;
10  return variants[variantIndex];
11}
12
13// Fonction utilitaire pour générer un hash à partir d'une chaîne
14function hashString(str: string): number {
15  let hash = 0;
16  for (let i = 0; i < str.length; i++) {
17    hash = ((hash << 5) - hash) + str.charCodeAt(i);
18    hash |= 0; // Convertit en entier 32 bits
19  }
20  return Math.abs(hash);
21}
22
23export function proxy(request: NextRequest) {
24  const pathname = request.nextUrl.pathname;
25
26  // Test A/B pour la page d'accueil
27  if (pathname === '/') {
28    // Récupère ou génère l'identifiant de l'utilisateur
29    let userId = request.cookies.get('user_id')?.value;
30
31    if (!userId) {
32      userId = crypto.randomUUID();
33      // Dans une vraie implémentation, ce cookie devrait être enregistré dans la réponse
34    }
35
36    // Détermine la variante du test A/B pour cet utilisateur
37    const homePageVariant = getABTestVariant(userId, 'homepage_redesign', ['control', 'variant_a', 'variant_b']);
38
39    // Redirige vers la variante appropriée
40    if (homePageVariant !== 'control') {
41      return NextResponse.redirect(new URL(`/home-${homePageVariant}`, request.url));
42    }
43  }
44
45  return NextResponse.next();
46}
47
48export const config = {
49  matcher: ['/'],
50};

Redirections basées sur le statut de l'API

On peut effectuer des redirections en fonction des réponses de l'API :

1// app/quantum-dashboard/page.tsx
2import { redirect } from 'next/navigation';
3import { getSystemStatus } from '@/lib/api';
4
5export default async function DashboardPage() {
6  let status;
7  try {
8    status = await getSystemStatus();
9  } catch (error) {
10    // Si l'API est indisponible, redirige vers la page d'état
11    redirect('/system-status');
12  }
13
14  // redirect() lève une erreur spéciale : on l'appelle donc en dehors de try/catch
15  if (status.maintenance) {
16    redirect('/maintenance');
17  }
18  if (status.requiresClientUpdate) {
19    redirect('/update-required');
20  }
21
22  return (
23    <div className="quantum-dashboard">
24      <h1>Panneau de contrôle</h1>
25      {/* Contenu du panneau */}
26    </div>
27  );
28}

Intégration avec le système d'analytics

Pour les redirections et les pages 404, il est utile de suivre quels chemins mènent à des erreurs ou à des redirections afin d'optimiser l'expérience utilisateur. Voici un exemple d'implémentation du suivi des pages 404 :

1// app/not-found.tsx
2'use client';
3
4import { useEffect } from 'react';
5import Link from 'next/link';
6import { usePathname } from 'next/navigation';
7import { trackEvent } from '@/lib/analytics';
8
9export default function NotFound() {
10  const pathname = usePathname();
11
12  useEffect(() => {
13    // Suit l'événement 404
14    trackEvent('404_error', {
15      path: pathname,
16      timestamp: new Date().toISOString(),
17      referrer: document.referrer
18    });
19  }, [pathname]);
20
21  return (
22    <div className="quantum-not-found">
23      <h1>404 : Emplacement quantique introuvable</h1>
24      {/* Contenu de la page 404 */}
25    </div>
26  );
27}

Tester les redirections et les pages 404

Il est important de tester minutieusement les redirections et les pages 404 pour s'assurer qu'elles fonctionnent comme prévu :

1// tests/redirects.test.js
2import { createServer } from 'http';
3import { parse } from 'url';
4import fetch from 'node-fetch';
5import { NextRequest } from 'next/server';
6import { proxy } from '../proxy';
7
8describe('Redirects Tests', () => {
9  let server;
10  let port;
11
12  beforeAll(() => {
13    // Démarre le serveur de test local
14    server = createServer((req, res) => {
15      res.writeHead(200).end('Test server');
16    }).listen(0);
17    port = server.address().port;
18  });
19
20  afterAll(() => {
21    server.close();
22  });
23
24  test('Should redirect /legacy-api to /api/v2', async () => {
25    const req = new NextRequest(new URL(`http://localhost:${port}/legacy-api/users`));
26    const res = proxy(req);
27
28    expect(res.status).toBe(307);
29    expect(res.headers.get('Location')).toBe(`http://localhost:${port}/api/v2/users`);
30  });
31
32  test('Should handle country-specific redirects', async () => {
33    const req = new NextRequest(new URL(`http://localhost:${port}/events`), {
34      headers: { 'x-vercel-ip-country': 'PL' },
35    });
36
37    const res = proxy(req);
38
39    expect(res.status).toBe(307);
40    expect(res.headers.get('Location')).toBe(`http://localhost:${port}/wydarzenia`);
41  });
42});

Résumé

Une bonne gestion des pages 404 et des redirections est un élément crucial de toute application Next.js professionnelle. Tout comme à Metropolis Quantum, où des systèmes de navigation avancés garantissent que chaque voyageur atteint le bon endroit (même si la destination d'origine n'existe pas), nous devons nous aussi assurer une expérience utilisateur fluide dans notre application, même en cas d'erreur.

Next.js 16 propose des outils complets pour implémenter :

  • Des pages 404 personnalisées, globales et spécifiques à un segment
  • Des redirections statiques dans la configuration
  • Des redirections dynamiques dans le code
  • Des règles de redirection avancées dans le proxy

Grâce à ces outils, on peut créer des applications intuitives et conviviales qui gèrent élégamment même les situations inattendues.

Dans la prochaine leçon, tu découvriras le proxy (anciennement middleware), qui intercepte les requêtes avant qu'elles n'atteignent la page.

Tu as repéré une erreur dans cette leçon ?

Teste tes connaissances

Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.

  1. 1. Qu'arrive-t-il au composant layout.tsx lors de la navigation entre des pages qui partagent la même mise en page ?

  2. 2. En quoi template.tsx diffère-t-il de layout.tsx dans l'App Router de Next.js ?

  3. 3. Le fichier not-found.js dans l'App Router s'affiche :

  4. 4. Les Intercepting Routes dans l'App Router permettent de :

Tâches pratiques dans le jeu

  • Éditeur de code

    Crée /app/dashboard/page.tsx : export default function Dashboard() { return <main className='dashboard-grid'><h1>Dashboard</h1><div className='lo-panel'>System Online</div></main> }. Tu verras le résultat dans l'aperçu à côté de l'éditeur.

  • Tri horizontal

    Reconstitue l'import du type Metadata depuis la bibliothèque Next.js

  • Éditeur de code

    Crée /app/login/page.tsx avec un formulaire : 'use client' tout en haut (useState est nécessaire), export default function LoginPage(), l'état const [email, setEmail] = useState(''), dans le formulaire <input value={email} onChange={e => setEmail(e.target.value)} /> et un bouton « Se connecter au réseau ». Ajoute des styles de base !

  • Éditeur de code

    Crée un Server Component async (export par défaut) : export default async function ImplantsDashboard() { const implants = await fetch('https://api.example.com/implants').then(r => r.json()); return <ul>{implants.map(i => <li key={i.id}>{i.name}</li>)}</ul> }. Côté serveur, fetch a besoin d'une URL complète. Pas de 'use client' = Server Component !

  • Tri vertical

    Remets dans l'ordre le rendu des composants dans l'App Router

  • Clique dans l'ordre

    Reconstitue la syntaxe de déclaration de l'objet metadata dans Next.js

  • Éditeur de code

    Dans le terminal, crée une structure avec le Route Group (marketing) contenant les pages about et contact : mkdir -p crée les dossiers app/(marketing)/about et app/(marketing)/contact, et touch ajoute un fichier page.tsx dans chacun d'eux. Mets les chemins avec des parenthèses entre guillemets, par ex. mkdir -p "app/(marketing)/about".

Articles utiles