Kurs Next.js · Moduł 2: Routing i layouty
Dynamiczne segmenty ścieżki i parametry
W tej lekcji7
W Metropolii Quantum najnowocześniejszy system transportowy posiada niezwykłą zdolność dostosowywania się do potrzeb obywateli. Trasy transportowe nie są sztywno określone - mogą dynamicznie reorganizować się, aby doprowadzić podróżnych do dowolnego miejsca w mieście. Ta elastyczność jest kluczowa dla efektywnego funkcjonowania tak złożonego organizmu miejskiego.
W świecie Next.js 16 analogiczną funkcję pełnią dynamiczne segmenty ścieżki, które pozwalają tworzyć trasy, które adaptują się do różnych parametrów i kontekstów. To one sprawiają, że nawigacja w aplikacji staje się prawdziwie interaktywna i dostosowana do potrzeb użytkownika.
Dynamiczne segmenty w App Routerze
W Next.js 16 dynamiczne segmenty ścieżki tworzy się poprzez umieszczenie nazwy parametru w nawiasach kwadratowych [param]. Taki folder będzie dopasowywał się do różnych wartości w tym segmencie URL, a wartość parametru będzie dostępna wewnątrz komponentów strony.
Podstawowe dynamiczne segmenty
Oto jak wygląda struktura plików dla aplikacji z dynamicznymi segmentami:
1app/
2 ├── destinations/
3 │ ├── page.tsx # pasuje do /destinations
4 │ └── [planetId]/ # pasuje do /destinations/:planetId (dynamiczny segment)
5 │ └── page.tsx # pasuje do /destinations/mars, /destinations/venus, etc.W tym przykładzie [planetId] to dynamiczny segment, który może przyjąć dowolną wartość, np. "mars", "venus", czy "jupiter". Ta wartość jest następnie dostępna w komponencie strony przez prop params. W Next.js 16 params to obietnica (Promise), więc odczytujesz ją przez const { planetId } = await params.
Implementacja strony z dynamicznym segmentem
Spójrzmy na implementację strony, która wykorzystuje dynamiczny segment [planetId]:
1// app/destinations/[planetId]/page.tsx
2import Image from 'next/image';
3import { notFound } from 'next/navigation';
4
5// Ten typ definiuje strukturę parametrów dla tej strony
6type PlanetPageParams = {
7 params: Promise<{
8 planetId: string;
9 }>;
10};
11
12async function getPlanetData(planetId: string) {
13 // Tutaj normalnie pobieralibyśmy dane z API lub bazy danych
14 // Na potrzeby przykładu używamy statycznych danych
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: 'Czerwona planeta znana z trudnych warunków i fascynujących zjawisk geologicznych.',
28 gravity: '3.721 m/s²',
29 diameter: '6,779 km',
30 dayLength: '24h 37m',
31 yearLength: '687 dni ziemskich',
32 temperature: '-63°C (średnia)',
33 imageUrl: '/images/mars.jpg'
34 },
35 'venus': {
36 name: 'Wenus',
37 description: 'Druga planeta od Słońca, znana z ekstremalnie gorącego klimatu i gęstej atmosfery.',
38 gravity: '8.87 m/s²',
39 diameter: '12,104 km',
40 dayLength: '243 dni ziemskie',
41 yearLength: '225 dni ziemskich',
42 temperature: '462°C (średnia)',
43 imageUrl: '/images/venus.jpg'
44 },
45 'jupiter': {
46 name: 'Jowisz',
47 description: 'Największa planeta Układu Słonecznego, znana z Wielkiej Czerwonej Plamy i licznych księżyców.',
48 gravity: '24.79 m/s²',
49 diameter: '139,820 km',
50 dayLength: '9h 56m',
51 yearLength: '11.86 lat ziemskich',
52 temperature: '-108°C (średnia)',
53 imageUrl: '/images/jupiter.jpg'
54 }
55 };
56
57 return planets[planetId];
58}
59
60// Komponent otrzymuje params jako props
61export default async function PlanetPage({ params }: PlanetPageParams) {
62 // Pobranie danych planety na podstawie planetId
63 const planet = await getPlanetData((await params).planetId);
64
65 // Jeśli planeta nie istnieje, wyświetl stronę 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">Dane planetarne</h2>
90 <table className="w-full">
91 <tbody>
92 <tr className="border-b">
93 <th className="text-left py-2">Grawitacja</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">Średnica</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">Długość dnia</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">Długość roku</th>
106 <td className="text-right py-2">{planet.yearLength}</td>
107 </tr>
108 <tr>
109 <th className="text-left py-2">Temperatura</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 Kolonie
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 Wycieczki
128 </a>
129 </div>
130 </div>
131 </div>
132 </div>
133 );
134}W powyższym kodzie:
- Definiujemy typ
PlanetPageParams, który określa strukturę parametrów otrzymywanych przez komponent - Komponent
PlanetPageotrzymuje obiektparamszawierającyplanetIdz URL - Używamy tego
planetIddo pobrania danych o planecie - Wyświetlamy szczegółowe informacje o planecie, a także linki do podstron, które również wykorzystują ten sam parametr
Generowanie statycznych stron dla znanych parametrów
Jeśli znamy z góry wszystkie możliwe wartości dla dynamicznego segmentu, możemy wygenerować statyczne strony podczas budowania aplikacji, co znacznie poprawi wydajność. Służy do tego funkcja generateStaticParams:
1// app/destinations/[planetId]/page.tsx
2
3// Ta funkcja definiuje statyczne ścieżki, które zostaną wygenerowane podczas budowania
4export async function generateStaticParams() {
5 // Tutaj normalnie pobieralibyśmy dane z API lub bazy danych
6 // Zwracamy tablicę obiektów z parametrami, które mają być wygenerowane statycznie
7 return [
8 { planetId: 'mars' },
9 { planetId: 'venus' },
10 { planetId: 'jupiter' }
11 ];
12}
13
14// Reszta komponentu...Dzięki temu Next.js wygeneruje trzy statyczne strony podczas budowania: dla Marsa, Wenus i Jowisza. To znacznie przyspieszy ich ładowanie, ponieważ nie będą musiały być generowane dynamicznie przy każdym żądaniu.
Wielopoziomowe dynamiczne segmenty
W Metropolii Quantum system transportowy umożliwia podróżowanie do dowolnego celu poprzez serię precyzyjnych przesiadek, z których każda zawęża przestrzeń do wybranego celu. Podobnie w Next.js 16 możemy tworzyć wielopoziomowe dynamiczne segmenty, aby dokładnie określić żądane zasoby.
Struktura z wieloma dynamicznymi segmentami
1app/
2 ├── destinations/
3 │ ├── [planetId]/ # pasuje do /destinations/:planetId
4 │ │ ├── page.tsx # pasuje do /destinations/mars
5 │ │ └── [moonId]/ # pasuje do /destinations/:planetId/:moonId
6 │ │ └── page.tsx # pasuje do /destinations/mars/phobos
7 │ └── page.tsx # pasuje do /destinationsW tej strukturze mamy dwa poziomy dynamicznych segmentów:
[planetId]- identyfikator planety[moonId]- identyfikator księżyca danej planety
Implementacja strony z zagnieżdżonymi parametrami
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 // System mapowania księżyców do planet
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: 'Większy i bliższy z dwóch naturalnych satelitów Marsa, krąży wokół planety co 7 godzin i 39 minut.',
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: 'Mniejszy i dalszy z dwóch naturalnych satelitów Marsa, o nieregularnym kształcie.',
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: 'Europa',
44 description: 'Księżyc Jowisza, który może posiadać podpowierzchniowy ocean wody pod lodową skorupą.',
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: 'Najbardziej aktywny wulkanicznie obiekt w Układzie Słonecznym, z ponad 400 aktywnymi wulkanami.',
53 diameter: '3,643.2 km',
54 orbitDistance: '421,700 km',
55 discovered: '1610',
56 imageUrl: '/images/io.jpg'
57 },
58 'ganymede': {
59 name: 'Ganimedes',
60 description: 'Największy księżyc w Układzie Słonecznym, większy nawet od planety Merkury.',
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 // Sprawdzenie, czy planeta i księżyc istnieją
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 Wróć do {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">Informacje</h2>
120 <table className="w-full">
121 <tbody>
122 <tr className="border-b">
123 <th className="text-left py-2">Planeta</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">Średnica</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">Odległość orbity</th>
132 <td className="text-right py-2">{moon.orbitDistance}</td>
133 </tr>
134 <tr>
135 <th className="text-left py-2">Odkryty</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 Badania naukowe
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 Miejsca lądowań
154 </a>
155 </div>
156 </div>
157 </div>
158 </div>
159 );
160}
161
162export async function generateStaticParams() {
163 // Generujemy wszystkie możliwe kombinacje planet i ich księżyców
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}W tym przykładzie:
- Komponent otrzymuje dwa parametry:
planetIdimoonId - Używamy obu parametrów do pobrania danych o konkretnym księżycu konkretnej planety
- Implementujemy nawigację z powrotem do strony planety
- Generujemy statycznie wszystkie znane kombinacje planet i księżyców
Parametry typu catch-all i opcjonalne catch-all
W Metropolii Quantum niektóre linie transportowe są tak elastyczne, że mogą docierać do dowolnej liczby punktów wzdłuż trasy, pobierając wszystkie istotne dane o trasie jako parametry podróży. W Next.js 16 odpowiednikami są segmenty typu catch-all i opcjonalne catch-all.
Segmenty catch-all ([...param])
Segment catch-all, oznaczony jako [...param], dopasowuje ścieżkę o dowolnej liczbie segmentów. Wszystkie dopasowane segmenty są przekazywane jako tablica.
1app/
2 ├── stellar-objects/
3 │ ├── [...path]/ # pasuje do /stellar-objects/any/number/of/segments
4 │ │ └── page.tsx
5 │ └── page.tsx # pasuje do /stellar-objectsImplementacja strony z segmentem 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">Przeglądarka obiektów kosmicznych</h1>
14
15 <div className="bg-gray-100 p-4 rounded mb-8">
16 <p className="font-mono">Ścieżka: /{path.join('/')}</p>
17 <p>Segmenty: {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 {/* Tutaj normalna zawartość strony... */}
26 </div>
27 );
28}Przykładowy URL: /stellar-objects/stars/main-sequence/g-type zostanie dopasowany do tej strony, a path odczytane przez await params będzie tablicą ["stars", "main-sequence", "g-type"].
Opcjonalne segmenty catch-all ([[...param]])
Opcjonalny segment catch-all, oznaczony jako [[...param]] (podwójne nawiasy kwadratowe), działa podobnie do standardowego catch-all, ale dopasowuje również ścieżkę bez dodatkowych segmentów.
1app/
2 ├── knowledge-base/
3 ├── [[...topic]]/ # pasuje do /knowledge-base, /knowledge-base/planets, /knowledge-base/planets/terrestrial, etc.
4 └── page.tsxImplementacja:
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 // Jeśli topic nie istnieje, to jesteśmy na głównej stronie bazy wiedzy
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 ? 'Baza wiedzy kosmicznej' : 'Temat: ' + topic.join(' > ')}
18 </h1>
19
20 {isRoot ? (
21 <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
22 <TopicCard title="Planety" href="/knowledge-base/planets" />
23 <TopicCard title="Gwiazdy" href="/knowledge-base/stars" />
24 <TopicCard title="Galaktyki" href="/knowledge-base/galaxies" />
25 {/* Więcej kart tematów... */}
26 </div>
27 ) : (
28 <div>
29 <div className="bg-gray-100 p-4 rounded mb-8">
30 <p>Przeglądasz temat: <strong>{topic.join(' > ')}</strong></p>
31
32 <div className="mt-4">
33 <a href="/knowledge-base" className="text-indigo-600 hover:underline">
34 Wróć do głównej strony bazy wiedzy
35 </a>
36 </div>
37 </div>
38
39 {/* Zawartość konkretnego tematu... */}
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">Odkryj wiedzę o {title.toLowerCase()}.</p>
51 </a>
52 );
53}Ta strona dopasuje zarówno /knowledge-base (główna strona bazy wiedzy), jak i /knowledge-base/planets/terrestrial (konkretny temat).
Parametry w Route Handlers
W Metropolii Quantum nie tylko linie transportowe dla obywateli używają parametrów trasy - również zautomatyzowane systemy komunikują się ze sobą poprzez dynamicznie konfigurowane kanały. W Next.js 16 odpowiednikiem są Route Handlers, które również mogą wykorzystywać dynamiczne segmenty.
Dynamiczne segmenty w API Routes
1app/
2 ├── api/
3 ├── planets/
4 ├── [id]/
5 │ └── route.ts # pasuje do /api/planets/:id
6 └── route.ts # pasuje do /api/planetsImplementacja Route Handler z dynamicznym segmentem:
1// app/api/planets/[id]/route.ts
2import { NextRequest, NextResponse } from 'next/server';
3
4// Typ dla parametrów
5type PlanetRouteContext = {
6 params: Promise<{
7 id: string;
8 }>;
9};
10
11// Przykładowe dane
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: 'Ziemia',
22 type: 'terrestrial',
23 habitable: true
24 },
25 'jupiter': {
26 id: 'jupiter',
27 name: 'Jowisz',
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 // Sprawdzenie, czy planeta istnieje
40 if (!planetData[id]) {
41 return NextResponse.json(
42 { error: 'Planeta nie znaleziona' },
43 { status: 404 }
44 );
45 }
46
47 // Zwrócenie danych planety
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 // Sprawdzenie, czy planeta istnieje
58 if (!planetData[id]) {
59 return NextResponse.json(
60 { error: 'Planeta nie znaleziona' },
61 { status: 404 }
62 );
63 }
64
65 try {
66 // Pobranie danych z żądania
67 const updateData = await request.json();
68
69 // Aktualizacja danych planety (tu tylko symulacja)
70 // W rzeczywistej aplikacji zapisalibyśmy to do bazy danych
71 const updatedPlanet = {
72 ...planetData[id],
73 ...updateData
74 };
75
76 // Zwrócenie zaktualizowanych danych
77 return NextResponse.json(updatedPlanet);
78 } catch (error) {
79 return NextResponse.json(
80 { error: 'Nieprawidłowe dane' },
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 // Sprawdzenie, czy planeta istnieje
93 if (!planetData[id]) {
94 return NextResponse.json(
95 { error: 'Planeta nie znaleziona' },
96 { status: 404 }
97 );
98 }
99
100 // Usunięcie planety (tu tylko symulacja)
101 // W rzeczywistej aplikacji usunęlibyśmy to z bazy danych
102
103 return NextResponse.json({ success: true });
104}W tym przykładzie:
- Route Handler obsługuje żądania dla konkretnej planety określonej przez
id - Implementujemy metody GET, PUT i DELETE dla zarządzania danymi planety
- Parametr
idjest dostępny przez obiektparamsprzekazywany do funkcji obsługujących
Pobieranie parametrów zapytania (query parameters)
W Metropolii Quantum, poza głównymi trasami, system transportowy może być dodatkowo konfigurowany przez opcjonalne parametry, takie jak "expressFlight" czy "comfortLevel". W świecie Next.js te opcjonalne ustawienia są reprezentowane przez parametry zapytania (query parameters).
Dostęp do parametrów zapytania w Server Components
1// app/search/page.tsx
2import { Suspense } from 'react';
3
4// To jest komponent serwerowy
5export default async function SearchPage({
6 searchParams
7}: {
8 searchParams: Promise<{ [key: string]: string | string[] | undefined }>
9}) {
10 // Pobranie parametrów zapytania
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">Wyszukiwarka kosmiczna</h1>
18
19 <div className="mb-8">
20 <p>Wyszukiwanie: <strong>{query}</strong></p>
21 <p>Kategoria: <strong>{category}</strong></p>
22 <p>Strona: <strong>{page}</strong></p>
23 </div>
24
25 <Suspense fallback={<div>Ładowanie wyników...</div>}>
26 <SearchResults query={query} category={category} page={page} />
27 </Suspense>
28 </div>
29 );
30}
31
32// Ten komponent pobiera i wyświetla wyniki
33async function SearchResults({
34 query,
35 category,
36 page
37}: {
38 query: string | string[],
39 category: string | string[],
40 page: number
41}) {
42 // Tutaj normalnie pobieralibyśmy dane na podstawie parametrów
43 // Symulacja opóźnienia dla demonstracji Suspense
44 await new Promise(resolve => setTimeout(resolve, 1000));
45
46 // Jeśli nie ma zapytania, wyświetl komunikat
47 if (!query) {
48 return (
49 <div className="bg-gray-100 p-8 rounded-lg text-center">
50 <p className="text-lg">Wpisz frazę, aby rozpocząć wyszukiwanie.</p>
51 </div>
52 );
53 }
54
55 // Tutaj normalnie wyświetlalibyśmy wyniki wyszukiwania
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">Wynik 1</h2>
60 <p>Znaleziono dla: {query}</p>
61 </div>
62 <div className="p-6 bg-white rounded-lg shadow">
63 <h2 className="text-xl font-semibold mb-2">Wynik 2</h2>
64 <p>Znaleziono dla: {query}</p>
65 </div>
66 <div className="p-6 bg-white rounded-lg shadow">
67 <h2 className="text-xl font-semibold mb-2">Wynik 3</h2>
68 <p>Znaleziono dla: {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 Poprzednia
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 Następna
86 </a>
87 </div>
88 </div>
89 );
90}W tym przykładzie:
- Komponent
SearchPageotrzymujesearchParamsjako props, zawierający wszystkie parametry zapytania z URL - Wykorzystujemy te parametry do konfiguracji wyszukiwania
- Implementujemy komponent
SearchResults, który pobiera dane na podstawie parametrów - Używamy
Suspensedo obsługi stanu ładowania
Dostęp do parametrów zapytania w Route Handlers
1// app/api/search/route.ts
2import { NextRequest, NextResponse } from 'next/server';
3
4export async function GET(request: NextRequest) {
5 // Pobranie URL z żądania
6 const { searchParams } = new URL(request.url);
7
8 // Pobranie parametrów zapytania
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 // Sprawdzenie czy query istnieje
15 if (!query) {
16 return NextResponse.json(
17 { error: 'Parametr q (zapytanie) jest wymagany' },
18 { status: 400 }
19 );
20 }
21
22 // Tutaj normalnie pobieralibyśmy dane z bazy danych
23 // Na potrzeby przykładu zwracamy symulowane dane
24 const results = Array.from({ length: limit }, (_, i) => ({
25 id: `result-${(page - 1) * limit + i + 1}`,
26 title: `Wynik ${(page - 1) * limit + i + 1} dla "${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, // Symulowana całkowita liczba wyników
37 results
38 });
39}W tym przykładzie:
- Używamy
new URL(request.url).searchParamsdo pobrania parametrów zapytania - Wyciągamy i walidujemy konkretne parametry
- Zwracamy dane odpowiednie dla podanych parametrów
Generowanie dynamicznych metadanych
W Metropolii Quantum każda linia transportowa posiada dynamicznie generowane informacje dotyczące trasy, dostępne poprzez system informacyjny miasta. W Next.js 16 możemy generować dynamiczne metadane dla każdej strony, korzystając z funkcji generateMetadata.
1// app/destinations/[planetId]/page.tsx
2import type { Metadata } from 'next';
3
4// Typ dla parametrów
5type PlanetParams = {
6 params: Promise<{
7 planetId: string;
8 }>;
9};
10
11// Funkcja generująca metadane na podstawie planetId
12export async function generateMetadata(
13 { params }: PlanetParams
14): Promise<Metadata> {
15 const { planetId } = await params;
16
17 // Pobieranie danych o planecie
18 const planet = await getPlanetData(planetId);
19
20 // Jeśli planeta nie istnieje, zwróć domyślne metadane
21 if (!planet) {
22 return {
23 title: 'Planeta nie znaleziona | Quantum Voyages',
24 description: 'Nie znaleziono informacji o żądanej planecie.'
25 };
26 }
27
28 // Zwróć metadane specyficzne dla planety
29 return {
30 title: `${planet.name} | Quantum Voyages`,
31 description: planet.description.substring(0, 160), // Ogranicz do 160 znaków dla SEO
32 openGraph: {
33 title: `Odkryj ${planet.name} z 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// Reszta kodu komponentu...W tym przykładzie:
generateMetadataotrzymuje te sameparamsco komponent strony- Używamy
planetIddo pobrania danych o planecie - Generujemy dynamiczne metadane, w tym tytuł, opis i dane dla Open Graph
- Metadane te są używane do zoptymalizowania SEO strony
Metadane są zbierane od głównego layoutu w dół, aż do strony: Next.js płytko scala obiekty z kolejnych segmentów (pola zdefiniowane w stronie nadpisują te same pola z layoutu), a wynik trafia do sekcji <head> dokumentu.
Podsumowanie
Dynamiczne segmenty ścieżki i parametry w Next.js 16 to potężne narzędzia, które umożliwiają tworzenie elastycznych i interaktywnych aplikacji. Podobnie jak w Metropolii Quantum, gdzie system transportowy adaptuje się do potrzeb obywateli, tak dynamiczny routing w Next.js dostosowuje się do potrzeb użytkowników.
Kluczowe punkty do zapamiętania:
- Dynamiczne segmenty tworzy się poprzez umieszczenie nazwy parametru w nawiasach kwadratowych
[param] - Wartości parametrów są dostępne w komponentach strony poprzez prop
params - Segmenty catch-all (
[...param]) umożliwiają dopasowanie wielu segmentów ścieżki - Opcjonalne segmenty catch-all (
[[...param]]) działają podobnie, ale dopasowują również ścieżkę bez dodatkowych segmentów - Parametry zapytania są dostępne przez prop
searchParamsw komponentach strony - Dynamiczne metadane mogą być generowane na podstawie parametrów ścieżki
Dzięki tym funkcjom możesz tworzyć zaawansowane aplikacje z intuicyjną nawigacją i bogatymi funkcjami dopasowanymi do konkretnych potrzeb użytkownika.
W następnym rozdziale odkryjemy, jak efektywnie nawigować pomiędzy różnymi stronami w naszej aplikacji przy użyciu komponentu Link i hooka useRouter.
Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Dynamiczne segmenty URL w App Routerze tworzy się używając:
2. Plik loading.js w App Routerze wyświetla się:
3. Plik error.js w App Routerze służy do:
Zadania praktyczne w grze
- Edytor kodu
Utwórz stronę app/product/[id]/page.js: wyeksportuj domyślnie async komponent ProductPage({ params }). W Next.js 16 params to Promise, więc odczytaj id przez const { id } = await params i wyświetl nagłówek h1 z tekstem „Produkt {id}”.
- Układanie w pionie
Ułóż składnię funkcji generateStaticParams w App Routerze
- Edytor kodu
Utwórz plik loading.js z eksportowanym domyślnie komponentem Loading, który wyświetla tekst „Ładowanie...” (możesz dodać obok spinner, np. div z klasą spinner).