Cours Next.js · Module 2: Routing et layouts
Segments de chemin dynamiques et paramètres
Dans cette leçon7
À Metropolis Quantum, le système de transport le plus avancé possède une capacité remarquable à s'adapter aux besoins de ses citoyens. Les itinéraires de transport ne sont pas figés - ils peuvent se réorganiser dynamiquement pour guider les voyageurs vers n'importe quel endroit de la ville. Cette flexibilité est essentielle au bon fonctionnement d'un organisme urbain aussi complexe.
Dans le monde de Next.js 16, les segments de chemin dynamiques remplissent une fonction analogue, te permettant de créer des routes qui s'adaptent à différents paramètres et contextes. Ce sont eux qui rendent la navigation dans une application vraiment interactive et adaptée aux besoins de l'utilisateur.
Les segments dynamiques dans l'App Router
Dans Next.js 16, les segments de chemin dynamiques se créent en plaçant le nom du paramètre entre crochets [param]. Un tel dossier correspondra à différentes valeurs dans ce segment d'URL, et la valeur du paramètre sera disponible à l'intérieur des composants de la page.
Segments dynamiques de base
Voici à quoi ressemble la structure de fichiers d'une application avec des segments dynamiques :
1app/
2 ├── destinations/
3 │ ├── page.tsx # correspond à /destinations
4 │ └── [planetId]/ # correspond à /destinations/:planetId (segment dynamique)
5 │ └── page.tsx # correspond à /destinations/mars, /destinations/venus, etc.Dans cet exemple, [planetId] est un segment dynamique qui peut accepter n'importe quelle valeur, par ex. « mars », « venus » ou « jupiter ». Cette valeur est ensuite disponible dans le composant de page via la prop params. Dans Next.js 16, params est une Promise : tu la lis avec const { planetId } = await params.
Implémenter une page avec un segment dynamique
Regardons l'implémentation d'une page qui utilise le segment dynamique [planetId] :
1// app/destinations/[planetId]/page.tsx
2import Image from 'next/image';
3import { notFound } from 'next/navigation';
4
5// Ce type définit la structure des paramètres pour cette page
6type PlanetPageParams = {
7 params: Promise<{
8 planetId: string;
9 }>;
10};
11
12async function getPlanetData(planetId: string) {
13 // Normalement, on récupérerait ici les données depuis une API ou une base de données
14 // Pour cet exemple, on utilise des données statiques
15 const planets: Record<string, {
16 name: string;
17 description: string;
18 gravity: string;
19 diameter: string;
20 dayLength: string;
21 yearLength: string;
22 temperature: string;
23 imageUrl: string;
24 }> = {
25 'mars': {
26 name: 'Mars',
27 description: 'La planète rouge, connue pour ses conditions extrêmes et ses phénomènes géologiques fascinants.',
28 gravity: '3.721 m/s²',
29 diameter: '6 779 km',
30 dayLength: '24h 37m',
31 yearLength: '687 jours terrestres',
32 temperature: '-63°C (moyenne)',
33 imageUrl: '/images/mars.jpg'
34 },
35 'venus': {
36 name: 'Vénus',
37 description: 'La deuxième planète à partir du Soleil, connue pour son climat extrêmement chaud et son atmosphère dense.',
38 gravity: '8.87 m/s²',
39 diameter: '12 104 km',
40 dayLength: '243 jours terrestres',
41 yearLength: '225 jours terrestres',
42 temperature: '462°C (moyenne)',
43 imageUrl: '/images/venus.jpg'
44 },
45 'jupiter': {
46 name: 'Jupiter',
47 description: 'La plus grande planète du Système solaire, connue pour la Grande Tache rouge et ses nombreuses lunes.',
48 gravity: '24.79 m/s²',
49 diameter: '139 820 km',
50 dayLength: '9h 56m',
51 yearLength: '11.86 années terrestres',
52 temperature: '-108°C (moyenne)',
53 imageUrl: '/images/jupiter.jpg'
54 }
55 };
56
57 return planets[planetId];
58}
59
60// Le composant reçoit params en props
61export default async function PlanetPage({ params }: PlanetPageParams) {
62 // Récupération des données de la planète à partir de planetId
63 const planet = await getPlanetData((await params).planetId);
64
65 // Si la planète n'existe pas, on affiche la page 404
66 if (!planet) {
67 notFound();
68 }
69
70 return (
71 <div className="container mx-auto px-4 py-12">
72 <h1 className="text-4xl font-bold mb-8">{planet.name}</h1>
73
74 <div className="grid grid-cols-1 lg:grid-cols-2 gap-12">
75 <div className="relative h-96 lg:h-auto rounded-lg overflow-hidden">
76 <Image
77 src={planet.imageUrl}
78 alt={planet.name}
79 fill
80 className="object-cover"
81 preload
82 />
83 </div>
84
85 <div className="space-y-6">
86 <p className="text-lg text-gray-700">{planet.description}</p>
87
88 <div className="bg-gray-50 p-6 rounded-lg shadow-sm">
89 <h2 className="text-2xl font-semibold mb-4">Données planétaires</h2>
90 <table className="w-full">
91 <tbody>
92 <tr className="border-b">
93 <th className="text-left py-2">Gravité</th>
94 <td className="text-right py-2">{planet.gravity}</td>
95 </tr>
96 <tr className="border-b">
97 <th className="text-left py-2">Diamètre</th>
98 <td className="text-right py-2">{planet.diameter}</td>
99 </tr>
100 <tr className="border-b">
101 <th className="text-left py-2">Durée du jour</th>
102 <td className="text-right py-2">{planet.dayLength}</td>
103 </tr>
104 <tr className="border-b">
105 <th className="text-left py-2">Durée de l'année</th>
106 <td className="text-right py-2">{planet.yearLength}</td>
107 </tr>
108 <tr>
109 <th className="text-left py-2">Température</th>
110 <td className="text-right py-2">{planet.temperature}</td>
111 </tr>
112 </tbody>
113 </table>
114 </div>
115
116 <div className="flex space-x-4">
117 <a
118 href={`/destinations/${(await params).planetId}/colonies`}
119 className="px-6 py-3 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors"
120 >
121 Colonies
122 </a>
123 <a
124 href={`/destinations/${(await params).planetId}/tours`}
125 className="px-6 py-3 border border-indigo-600 text-indigo-600 rounded-lg hover:bg-indigo-50 transition-colors"
126 >
127 Excursions
128 </a>
129 </div>
130 </div>
131 </div>
132 </div>
133 );
134}Dans le code ci-dessus :
- On définit le type
PlanetPageParams, qui précise la structure des paramètres reçus par le composant - Le composant
PlanetPagereçoit un objetparamscontenantplanetIdissu de l'URL - On utilise ce
planetIdpour récupérer les données de la planète - On affiche les informations détaillées sur la planète, ainsi que des liens vers des sous-pages qui utilisent elles aussi le même paramètre
Générer des pages statiques pour des paramètres connus
Si l'on connaît à l'avance toutes les valeurs possibles d'un segment dynamique, on peut générer des pages statiques lors du build, ce qui améliore considérablement les performances. La fonction generateStaticParams sert à cela :
1// app/destinations/[planetId]/page.tsx
2
3// Cette fonction définit les chemins statiques qui seront générés lors du build
4export async function generateStaticParams() {
5 // Normalement, on récupérerait ici les données depuis une API ou une base de données
6 // On renvoie un tableau d'objets avec les paramètres à générer statiquement
7 return [
8 { planetId: 'mars' },
9 { planetId: 'venus' },
10 { planetId: 'jupiter' }
11 ];
12}
13
14// Le reste du composant...Grâce à cela, Next.js générera trois pages statiques lors du build : pour Mars, Vénus et Jupiter. Cela accélérera considérablement leur chargement, car elles n'auront pas besoin d'être générées dynamiquement à chaque requête.
Segments dynamiques multi-niveaux
À Metropolis Quantum, le système de transport permet de voyager vers n'importe quelle destination grâce à une série de correspondances précises, chacune réduisant l'espace jusqu'à la cible choisie. De la même manière, dans Next.js 16, on peut créer des segments dynamiques multi-niveaux pour spécifier précisément les ressources demandées.
Structure avec plusieurs segments dynamiques
1app/
2 ├── destinations/
3 │ ├── [planetId]/ # correspond à /destinations/:planetId
4 │ │ ├── page.tsx # correspond à /destinations/mars
5 │ │ └── [moonId]/ # correspond à /destinations/:planetId/:moonId
6 │ │ └── page.tsx # correspond à /destinations/mars/phobos
7 │ └── page.tsx # correspond à /destinationsDans cette structure, on a deux niveaux de segments dynamiques :
[planetId]- identifiant de la planète[moonId]- identifiant de la lune pour la planète donnée
Implémenter une page avec des paramètres imbriqués
1// app/destinations/[planetId]/[moonId]/page.tsx
2import Image from 'next/image';
3import Link from 'next/link';
4import { notFound } from 'next/navigation';
5
6type MoonPageParams = {
7 params: Promise<{
8 planetId: string;
9 moonId: string;
10 }>;
11};
12
13async function getMoonData(planetId: string, moonId: string) {
14 // Système d'association lune-planète
15 const moonData: Record<string, Record<string, {
16 name: string;
17 description: string;
18 diameter: string;
19 orbitDistance: string;
20 discovered: string;
21 imageUrl: string;
22 }>> = {
23 'mars': {
24 'phobos': {
25 name: 'Phobos',
26 description: 'Le plus grand et le plus proche des deux satellites naturels de Mars, orbitant autour de la planète toutes les 7 heures et 39 minutes.',
27 diameter: '22.2 km',
28 orbitDistance: '9 376 km',
29 discovered: '1877',
30 imageUrl: '/images/phobos.jpg'
31 },
32 'deimos': {
33 name: 'Deimos',
34 description: 'Le plus petit et le plus éloigné des deux satellites naturels de Mars, à la forme irrégulière.',
35 diameter: '12.6 km',
36 orbitDistance: '23 463 km',
37 discovered: '1877',
38 imageUrl: '/images/deimos.jpg'
39 }
40 },
41 'jupiter': {
42 'europa': {
43 name: 'Europe',
44 description: "La lune de Jupiter qui pourrait abriter un océan d'eau souterrain sous sa croûte glacée.",
45 diameter: '3 121.6 km',
46 orbitDistance: '670 900 km',
47 discovered: '1610',
48 imageUrl: '/images/europa.jpg'
49 },
50 'io': {
51 name: 'Io',
52 description: "L'objet le plus actif volcaniquement du Système solaire, avec plus de 400 volcans actifs.",
53 diameter: '3 643.2 km',
54 orbitDistance: '421 700 km',
55 discovered: '1610',
56 imageUrl: '/images/io.jpg'
57 },
58 'ganymede': {
59 name: 'Ganymède',
60 description: 'La plus grande lune du Système solaire, encore plus grande que la planète Mercure.',
61 diameter: '5 268.2 km',
62 orbitDistance: '1 070 400 km',
63 discovered: '1610',
64 imageUrl: '/images/ganymede.jpg'
65 }
66 }
67 };
68
69 // Vérifie si la planète et la lune existent
70 if (!moonData[planetId] || !moonData[planetId][moonId]) {
71 return null;
72 }
73
74 return {
75 ...moonData[planetId][moonId],
76 planet: planetId
77 };
78}
79
80export default async function MoonPage({ params }: MoonPageParams) {
81 const { planetId, moonId } = await params;
82 const moon = await getMoonData(planetId, moonId);
83
84 if (!moon) {
85 notFound();
86 }
87
88 return (
89 <div className="container mx-auto px-4 py-12">
90 <div className="flex items-center mb-8">
91 <Link
92 href={`/destinations/${planetId}`}
93 className="text-indigo-600 hover:underline flex items-center"
94 >
95 <svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
96 <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
97 </svg>
98 Retour vers {planetId.charAt(0).toUpperCase() + planetId.slice(1)}
99 </Link>
100 </div>
101
102 <h1 className="text-4xl font-bold mb-8">{moon.name}</h1>
103
104 <div className="grid grid-cols-1 lg:grid-cols-2 gap-12">
105 <div className="relative h-96 lg:h-auto rounded-lg overflow-hidden">
106 <Image
107 src={moon.imageUrl}
108 alt={moon.name}
109 fill
110 className="object-cover"
111 preload
112 />
113 </div>
114
115 <div className="space-y-6">
116 <p className="text-lg text-gray-700">{moon.description}</p>
117
118 <div className="bg-gray-50 p-6 rounded-lg shadow-sm">
119 <h2 className="text-2xl font-semibold mb-4">Informations</h2>
120 <table className="w-full">
121 <tbody>
122 <tr className="border-b">
123 <th className="text-left py-2">Planète</th>
124 <td className="text-right py-2">{moon.planet.charAt(0).toUpperCase() + moon.planet.slice(1)}</td>
125 </tr>
126 <tr className="border-b">
127 <th className="text-left py-2">Diamètre</th>
128 <td className="text-right py-2">{moon.diameter}</td>
129 </tr>
130 <tr className="border-b">
131 <th className="text-left py-2">Distance orbitale</th>
132 <td className="text-right py-2">{moon.orbitDistance}</td>
133 </tr>
134 <tr>
135 <th className="text-left py-2">Découverte</th>
136 <td className="text-right py-2">{moon.discovered}</td>
137 </tr>
138 </tbody>
139 </table>
140 </div>
141
142 <div className="flex space-x-4">
143 <a
144 href={`/destinations/${planetId}/${moonId}/research`}
145 className="px-6 py-3 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors"
146 >
147 Recherche scientifique
148 </a>
149 <a
150 href={`/destinations/${planetId}/${moonId}/landing-sites`}
151 className="px-6 py-3 border border-indigo-600 text-indigo-600 rounded-lg hover:bg-indigo-50 transition-colors"
152 >
153 Sites d'atterrissage
154 </a>
155 </div>
156 </div>
157 </div>
158 </div>
159 );
160}
161
162export async function generateStaticParams() {
163 // On génère toutes les combinaisons possibles de planètes et de leurs lunes
164 return [
165 { planetId: 'mars', moonId: 'phobos' },
166 { planetId: 'mars', moonId: 'deimos' },
167 { planetId: 'jupiter', moonId: 'europa' },
168 { planetId: 'jupiter', moonId: 'io' },
169 { planetId: 'jupiter', moonId: 'ganymede' }
170 ];
171}Dans cet exemple :
- Le composant reçoit deux paramètres :
planetIdetmoonId - On utilise les deux paramètres pour récupérer les données d'une lune précise d'une planète précise
- On implémente la navigation de retour vers la page de la planète
- On génère statiquement toutes les combinaisons connues de planètes et de lunes
Paramètres catch-all et optional catch-all
À Metropolis Quantum, certaines lignes de transport sont si flexibles qu'elles peuvent atteindre un nombre quelconque de points le long de l'itinéraire, collectant toutes les données de route pertinentes sous forme de paramètres de voyage. Dans Next.js 16, leurs équivalents sont les segments catch-all et optional catch-all.
Segments catch-all ([...param])
Un segment catch-all, noté [...param], correspond à un chemin comportant un nombre quelconque de segments. Tous les segments correspondants sont passés sous forme de tableau.
1app/
2 ├── stellar-objects/
3 │ ├── [...path]/ # correspond à /stellar-objects/n/importe/quel/nombre/de/segments
4 │ │ └── page.tsx
5 │ └── page.tsx # correspond à /stellar-objectsImplémentation d'une page avec un segment catch-all :
1// app/stellar-objects/[...path]/page.tsx
2type CatchAllParams = {
3 params: Promise<{
4 path: string[];
5 }>;
6};
7
8export default async function StellarObjectsPage({ params }: CatchAllParams) {
9 const { path } = await params;
10
11 return (
12 <div className="container mx-auto px-4 py-12">
13 <h1 className="text-4xl font-bold mb-8">Navigateur d'objets stellaires</h1>
14
15 <div className="bg-gray-100 p-4 rounded mb-8">
16 <p className="font-mono">Chemin : /{path.join('/')}</p>
17 <p>Segments : {path.length}</p>
18 <ul className="list-disc pl-6 mt-2">
19 {path.map((segment, index) => (
20 <li key={index}>Segment {index + 1} : {segment}</li>
21 ))}
22 </ul>
23 </div>
24
25 {/* Contenu normal de la page ici... */}
26 </div>
27 );
28}Exemple d'URL : /stellar-objects/stars/main-sequence/g-type sera associée à cette page, et path lu avec await params sera le tableau ["stars", "main-sequence", "g-type"].
Segments catch-all optionnels ([[...param]])
Un segment optional catch-all, noté [[...param]] (doubles crochets), fonctionne de manière similaire au catch-all standard, mais correspond aussi au chemin sans aucun segment supplémentaire.
1app/
2 ├── knowledge-base/
3 ├── [[...topic]]/ # correspond à /knowledge-base, /knowledge-base/planets, /knowledge-base/planets/terrestrial, etc.
4 └── page.tsxImplémentation :
1// app/knowledge-base/[[...topic]]/page.tsx
2type OptionalCatchAllParams = {
3 params: Promise<{
4 topic?: string[];
5 }>;
6};
7
8export default async function KnowledgeBasePage({ params }: OptionalCatchAllParams) {
9 const { topic } = await params;
10
11 // Si topic n'existe pas, on est sur la page principale de la base de connaissances
12 const isRoot = !topic || topic.length === 0;
13
14 return (
15 <div className="container mx-auto px-4 py-12">
16 <h1 className="text-4xl font-bold mb-8">
17 {isRoot ? 'Base de connaissances spatiales' : 'Sujet : ' + topic.join(' > ')}
18 </h1>
19
20 {isRoot ? (
21 <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
22 <TopicCard title="Planètes" href="/knowledge-base/planets" />
23 <TopicCard title="Étoiles" href="/knowledge-base/stars" />
24 <TopicCard title="Galaxies" href="/knowledge-base/galaxies" />
25 {/* Plus de cartes de sujets... */}
26 </div>
27 ) : (
28 <div>
29 <div className="bg-gray-100 p-4 rounded mb-8">
30 <p>Tu consultes le sujet : <strong>{topic.join(' > ')}</strong></p>
31
32 <div className="mt-4">
33 <a href="/knowledge-base" className="text-indigo-600 hover:underline">
34 Retourner à la page principale de la base de connaissances
35 </a>
36 </div>
37 </div>
38
39 {/* Contenu spécifique au sujet... */}
40 </div>
41 )}
42 </div>
43 );
44}
45
46function TopicCard({ title, href }: { title: string; href: string }) {
47 return (
48 <a href={href} className="block p-6 bg-white rounded-lg shadow-md hover:shadow-lg transition-shadow">
49 <h2 className="text-xl font-semibold mb-2">{title}</h2>
50 <p className="text-gray-600">Découvre des connaissances sur {title.toLowerCase()}.</p>
51 </a>
52 );
53}Cette page correspondra à la fois à /knowledge-base (page principale de la base de connaissances) et à /knowledge-base/planets/terrestrial (sujet spécifique).
Paramètres dans les Route Handlers
À Metropolis Quantum, ce ne sont pas seulement les lignes de transport des citoyens qui utilisent des paramètres de route - les systèmes automatisés communiquent aussi entre eux via des canaux configurés dynamiquement. Dans Next.js 16, l'équivalent est constitué des Route Handlers, qui peuvent eux aussi utiliser des segments dynamiques.
Segments dynamiques dans les routes d'API
1app/
2 ├── api/
3 ├── planets/
4 ├── [id]/
5 │ └── route.ts # correspond à /api/planets/:id
6 └── route.ts # correspond à /api/planetsImplémentation d'un Route Handler avec un segment dynamique :
1// app/api/planets/[id]/route.ts
2import { NextRequest, NextResponse } from 'next/server';
3
4// Type pour les paramètres
5type PlanetRouteContext = {
6 params: Promise<{
7 id: string;
8 }>;
9};
10
11// Données d'exemple
12const planetData: Record<string, { id: string; name: string; type: string; habitable: boolean }> = {
13 'mars': {
14 id: 'mars',
15 name: 'Mars',
16 type: 'terrestrial',
17 habitable: false
18 },
19 'earth': {
20 id: 'earth',
21 name: 'Earth',
22 type: 'terrestrial',
23 habitable: true
24 },
25 'jupiter': {
26 id: 'jupiter',
27 name: 'Jupiter',
28 type: 'gas-giant',
29 habitable: false
30 }
31};
32
33export async function GET(
34 request: NextRequest,
35 { params }: PlanetRouteContext
36) {
37 const { id } = await params;
38
39 // Vérifie si la planète existe
40 if (!planetData[id]) {
41 return NextResponse.json(
42 { error: 'Planet not found' },
43 { status: 404 }
44 );
45 }
46
47 // Renvoie les données de la planète
48 return NextResponse.json(planetData[id]);
49}
50
51export async function PUT(
52 request: NextRequest,
53 { params }: PlanetRouteContext
54) {
55 const { id } = await params;
56
57 // Vérifie si la planète existe
58 if (!planetData[id]) {
59 return NextResponse.json(
60 { error: 'Planet not found' },
61 { status: 404 }
62 );
63 }
64
65 try {
66 // Parse les données de la requête
67 const updateData = await request.json();
68
69 // Met à jour les données de la planète (simulation uniquement)
70 // Dans une vraie application, on enregistrerait cela en base de données
71 const updatedPlanet = {
72 ...planetData[id],
73 ...updateData
74 };
75
76 // Renvoie les données mises à jour
77 return NextResponse.json(updatedPlanet);
78 } catch (error) {
79 return NextResponse.json(
80 { error: 'Invalid data' },
81 { status: 400 }
82 );
83 }
84}
85
86export async function DELETE(
87 request: NextRequest,
88 { params }: PlanetRouteContext
89) {
90 const { id } = await params;
91
92 // Vérifie si la planète existe
93 if (!planetData[id]) {
94 return NextResponse.json(
95 { error: 'Planet not found' },
96 { status: 404 }
97 );
98 }
99
100 // Supprime la planète (simulation uniquement)
101 // Dans une vraie application, on la supprimerait de la base de données
102
103 return NextResponse.json({ success: true });
104}Dans cet exemple :
- Le Route Handler gère les requêtes pour une planète précise identifiée par
id - On implémente les méthodes GET, PUT et DELETE pour gérer les données des planètes
- Le paramètre
idest disponible via l'objetparamspassé aux fonctions du handler
Récupérer les paramètres de requête
À Metropolis Quantum, au-delà des itinéraires principaux, le système de transport peut être configuré en plus via des paramètres optionnels comme « expressFlight » ou « comfortLevel ». Dans le monde de Next.js, ces réglages optionnels sont représentés par les paramètres de requête (query parameters).
Accéder aux paramètres de requête dans les Server Components
1// app/search/page.tsx
2import { Suspense } from 'react';
3
4// Ceci est un composant serveur
5export default async function SearchPage({
6 searchParams
7}: {
8 searchParams: Promise<{ [key: string]: string | string[] | undefined }>
9}) {
10 // Récupère les paramètres de requête
11 const query = (await searchParams).q || '';
12 const category = (await searchParams).category || 'all';
13 const page = Number((await searchParams).page) || 1;
14
15 return (
16 <div className="container mx-auto px-4 py-12">
17 <h1 className="text-4xl font-bold mb-8">Moteur de recherche spatial</h1>
18
19 <div className="mb-8">
20 <p>Recherche : <strong>{query}</strong></p>
21 <p>Catégorie : <strong>{category}</strong></p>
22 <p>Page : <strong>{page}</strong></p>
23 </div>
24
25 <Suspense fallback={<div>Chargement des résultats...</div>}>
26 <SearchResults query={query} category={category} page={page} />
27 </Suspense>
28 </div>
29 );
30}
31
32// Ce composant récupère et affiche les résultats
33async function SearchResults({
34 query,
35 category,
36 page
37}: {
38 query: string | string[],
39 category: string | string[],
40 page: number
41}) {
42 // Normalement, on récupérerait les données en fonction des paramètres
43 // Simulation d'un délai pour la démonstration de Suspense
44 await new Promise(resolve => setTimeout(resolve, 1000));
45
46 // S'il n'y a pas de requête, on affiche un message
47 if (!query) {
48 return (
49 <div className="bg-gray-100 p-8 rounded-lg text-center">
50 <p className="text-lg">Saisis une expression pour commencer la recherche.</p>
51 </div>
52 );
53 }
54
55 // Normalement, on afficherait ici les résultats de recherche
56 return (
57 <div className="space-y-6">
58 <div className="p-6 bg-white rounded-lg shadow">
59 <h2 className="text-xl font-semibold mb-2">Résultat 1</h2>
60 <p>Trouvé pour : {query}</p>
61 </div>
62 <div className="p-6 bg-white rounded-lg shadow">
63 <h2 className="text-xl font-semibold mb-2">Résultat 2</h2>
64 <p>Trouvé pour : {query}</p>
65 </div>
66 <div className="p-6 bg-white rounded-lg shadow">
67 <h2 className="text-xl font-semibold mb-2">Résultat 3</h2>
68 <p>Trouvé pour : {query}</p>
69 </div>
70
71 <div className="flex justify-center space-x-2 mt-8">
72 <a
73 href={`/search?q=${query}&category=${category}&page=${Math.max(1, page - 1)}`}
74 className={`px-4 py-2 border rounded ${page === 1 ? 'text-gray-400 cursor-not-allowed' : 'text-indigo-600 hover:bg-indigo-50'}`}
75 >
76 Précédent
77 </a>
78 <span className="px-4 py-2 bg-indigo-600 text-white rounded">
79 {page}
80 </span>
81 <a
82 href={`/search?q=${query}&category=${category}&page=${page + 1}`}
83 className="px-4 py-2 border rounded text-indigo-600 hover:bg-indigo-50"
84 >
85 Suivant
86 </a>
87 </div>
88 </div>
89 );
90}Dans cet exemple :
- Le composant
SearchPagereçoitsearchParamsen props, contenant tous les paramètres de requête de l'URL - On utilise ces paramètres pour configurer la recherche
- On implémente le composant
SearchResults, qui récupère les données en fonction des paramètres - On utilise
Suspensepour gérer l'état de chargement
Accéder aux paramètres de requête dans les Route Handlers
1// app/api/search/route.ts
2import { NextRequest, NextResponse } from 'next/server';
3
4export async function GET(request: NextRequest) {
5 // Récupère l'URL de la requête
6 const { searchParams } = new URL(request.url);
7
8 // Récupère les paramètres de requête
9 const query = searchParams.get('q');
10 const category = searchParams.get('category') || 'all';
11 const page = Number(searchParams.get('page')) || 1;
12 const limit = Number(searchParams.get('limit')) || 10;
13
14 // Vérifie si query existe
15 if (!query) {
16 return NextResponse.json(
17 { error: 'The q (query) parameter is required' },
18 { status: 400 }
19 );
20 }
21
22 // Ici, on récupérerait normalement les données depuis une base de données
23 // Pour les besoins de cet exemple, on renvoie des données simulées
24 const results = Array.from({ length: limit }, (_, i) => ({
25 id: `result-${(page - 1) * limit + i + 1}`,
26 title: `Résultat ${(page - 1) * limit + i + 1} pour "${query}"`,
27 category,
28 relevance: Math.round(Math.random() * 100)
29 }));
30
31 return NextResponse.json({
32 query,
33 category,
34 page,
35 limit,
36 totalResults: 100, // Nombre total de résultats simulé
37 results
38 });
39}Dans cet exemple :
- On utilise
new URL(request.url).searchParamspour récupérer les paramètres de requête - On extrait et on valide des paramètres précis
- On renvoie les données adaptées aux paramètres donnés
Générer des métadonnées dynamiques
À Metropolis Quantum, chaque ligne de transport possède des informations d'itinéraire générées dynamiquement, accessibles via le système d'information de la ville. Dans Next.js 16, on peut générer des métadonnées dynamiques pour chaque page à l'aide de la fonction generateMetadata.
1// app/destinations/[planetId]/page.tsx
2import type { Metadata } from 'next';
3
4// Type pour les paramètres
5type PlanetParams = {
6 params: Promise<{
7 planetId: string;
8 }>;
9};
10
11// Fonction générant les métadonnées à partir de planetId
12export async function generateMetadata(
13 { params }: PlanetParams
14): Promise<Metadata> {
15 const { planetId } = await params;
16
17 // Récupération des données de la planète
18 const planet = await getPlanetData(planetId);
19
20 // Si la planète n'existe pas, on renvoie des métadonnées par défaut
21 if (!planet) {
22 return {
23 title: 'Planète introuvable | Quantum Voyages',
24 description: 'Aucune information trouvée sur la planète demandée.'
25 };
26 }
27
28 // Renvoie les métadonnées spécifiques à la planète
29 return {
30 title: `${planet.name} | Quantum Voyages`,
31 description: planet.description.substring(0, 160), // Limite à 160 caractères pour le SEO
32 openGraph: {
33 title: `Découvre ${planet.name} avec Quantum Voyages`,
34 description: planet.description.substring(0, 160),
35 images: [
36 {
37 url: `https://example.com${planet.imageUrl}`,
38 width: 1200,
39 height: 630,
40 alt: planet.name
41 }
42 ]
43 }
44 };
45}
46
47// Le reste du code du composant...Dans cet exemple :
generateMetadatareçoit les mêmesparamsque le composant de la page- On utilise
planetIdpour récupérer les données de la planète - On génère des métadonnées dynamiques, dont le titre, la description et les données Open Graph
- Ces métadonnées servent à optimiser le SEO de la page
Les métadonnées sont collectées du layout racine jusqu'à la page : Next.js fusionne superficiellement les objets des segments successifs (les champs définis dans la page remplacent les mêmes champs du layout), et le résultat va dans la section <head> du document.
Résumé
Les segments de chemin dynamiques et les paramètres dans Next.js 16 sont des outils puissants qui permettent de créer des applications flexibles et interactives. Tout comme à Metropolis Quantum, où le système de transport s'adapte aux besoins de ses citoyens, le routage dynamique dans Next.js s'adapte aux besoins de l'utilisateur.
Points clés à retenir :
- Les segments dynamiques se créent en plaçant le nom du paramètre entre crochets
[param] - Les valeurs des paramètres sont disponibles dans les composants de page via la prop
params - Les segments catch-all (
[...param]) permettent de faire correspondre plusieurs segments de chemin - Les segments optional catch-all (
[[...param]]) fonctionnent de manière similaire mais correspondent aussi au chemin sans segments supplémentaires - Les paramètres de requête sont disponibles via la prop
searchParamsdans les composants de page - Les métadonnées dynamiques peuvent être générées à partir des paramètres de chemin
Grâce à ces fonctionnalités, tu peux construire des applications avancées avec une navigation intuitive et des fonctionnalités riches, adaptées aux besoins précis des utilisateurs.
Dans le prochain chapitre, nous découvrirons comment naviguer efficacement entre les différentes pages de notre application à l'aide du composant Link et du hook useRouter.
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. Les segments d'URL dynamiques dans l'App Router se créent avec :
2. Le fichier loading.js dans l'App Router s'affiche :
3. Le fichier error.js dans l'App Router sert à :
Tâches pratiques dans le jeu
- Éditeur de code
Crée la page app/product/[id]/page.js : exporte par défaut un composant async ProductPage({ params }). Dans Next.js 16, params est une Promise : lis id avec const { id } = await params et affiche un titre h1 avec le texte « Produit {id} ».
- Tri vertical
Reconstitue la syntaxe de la fonction generateStaticParams dans l'App Router
- Éditeur de code
Crée un fichier loading.js avec un composant Loading exporté par défaut qui affiche le texte « Chargement... » (tu peux ajouter un spinner à côté, par ex. un div avec la classe spinner).