Utilizziamo i cookie per migliorare la tua esperienza sul sito
CodeWorlds

Zagnieżdżone ścieżki i parametry catch-all

W Metropolii Quantum, system transportowy umożliwia mieszkańcom dotarcie do każdego zakątka miasta, a nawet do niezbadanych jeszcze terytoriów. Specjalne pojazdy Q-Hoppery potrafią zebrać wiele celów podróży i odwiedzić je wszystkie po kolei, dostosowując się dynamicznie do potrzeb pasażerów. Ta elastyczność jest kluczowa dla funkcjonowania tak zaawansowanej struktury miejskiej.

W świecie Next.js 15, podobną elastyczność zapewniają zaawansowane funkcje routingu, takie jak zagnieżdżone ścieżki i parametry catch-all. Pozwalają one na obsługę złożonych struktur URL i dynamiczne generowanie tras w oparciu o dane.

Zagnieżdżone ścieżki dynamiczne

W Next.js 15, możemy tworzyć zagnieżdżone ścieżki dynamiczne, łącząc segmenty statyczne i dynamiczne na różnych poziomach hierarchii:

1app/
2  ├── destinations/              # /destinations
3  │   ├── page.tsx               # Strona z listą destynacji
4  │   └── [destinationId]/       # Dynamiczny segment dla ID destynacji
5  │       ├── page.tsx           # /destinations/:destinationId
6  │       └── attractions/       # /destinations/:destinationId/attractions
7  │           ├── page.tsx       # Strona z atrakcjami dla destynacji
8  │           └── [attractionId] # Zagnieżdżony segment dynamiczny
9  │               └── page.tsx   # /destinations/:destinationId/attractions/:attractionId

W tej strukturze, mamy dwa poziomy dynamicznych segmentów:

[destinationId]
i
[attractionId]
. To pozwala na tworzenie ścieżek takich jak
/destinations/mars/attractions/olympus-mons
.

Implementacja zagnieżdżonych ścieżek dynamicznych

Oto przykład jak zaimplementować system zagnieżdżonych ścieżek dla naszej aplikacji Quantum Voyages:

1// app/destinations/[destinationId]/page.tsx
2import { getDestinationById } from '@/lib/api';
3import { DestinationDetails } from '@/components/destinations/DestinationDetails';
4import { AttractionsPreview } from '@/components/destinations/AttractionsPreview';
5import Link from 'next/link';
6import { notFound } from 'next/navigation';
7
8// Generowanie statycznych parametrów dla najpopularniejszych destynacji
9export async function generateStaticParams() {
10  // Pobierz najpopularniejsze destynacje dla prerendering
11  const destinations = await getPopularDestinations();
12  
13  return destinations.map((destination) => ({
14    destinationId: destination.id,
15  }));
16}
17
18export default async function DestinationPage({ params }: { params: Promise<{ destinationId: string }> }) {
19  const { destinationId } = await params;
20  const destination = await getDestinationById(destinationId);
21  
22  // Jeśli destynacja nie istnieje, wyświetl stronę 404
23  if (!destination) {
24    notFound();
25  }
26  
27  return (
28    <div className="container mx-auto px-4 py-12">
29      <h1 className="text-4xl font-bold mb-6">{destination.name}</h1>
30      
31      <DestinationDetails destination={destination} />
32      
33      <section className="mt-12">
34        <div className="flex justify-between items-center mb-6">
35          <h2 className="text-2xl font-semibold">Popularne atrakcje</h2>
36          <Link 
37            href={`/destinations/\${destinationId}/attractions\`}
38            className="text-indigo-600 hover:underline"
39          >
40            Zobacz wszystkie atrakcje
41          </Link>
42        </div>
43        
44        <AttractionsPreview destinationId={destinationId} />
45      </section>
46    </div>
47  );
48}
1// app/destinations/[destinationId]/attractions/page.tsx
2import { getDestinationById, getAttractionsForDestination } from '@/lib/api';
3import Link from 'next/link';
4import Image from 'next/image';
5import { notFound } from 'next/navigation';
6
7export default async function DestinationAttractionsPage({ params }: { params: Promise<{ destinationId: string }> }) {
8  const { destinationId } = await params;
9  const destination = await getDestinationById(destinationId);
10  
11  if (!destination) {
12    notFound();
13  }
14  
15  const attractions = await getAttractionsForDestination(destinationId);
16  
17  return (
18    <div className="container mx-auto px-4 py-12">
19      <div className="flex items-center gap-2 mb-8 text-sm text-gray-500">
20        <Link href="/destinations" className="hover:text-indigo-600">Destynacje</Link>
21        <span>/</span>
22        <Link href={`/destinations/\${destinationId}\`} className="hover:text-indigo-600">
23          {destination.name}
24        </Link>
25        <span>/</span>
26        <span>Atrakcje</span>
27      </div>
28      
29      <h1 className="text-3xl font-bold mb-2">Atrakcje na {destination.name}</h1>
30      <p className="text-gray-600 mb-8">{destination.description}</p>
31      
32      <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
33        {attractions.map((attraction) => (
34          <Link
35            key={attraction.id}
36            href={`/destinations/\${destinationId}/attractions/\${attraction.id}\`}
37            className="block group"
38          >
39            <div className="rounded-lg overflow-hidden shadow-md hover:shadow-lg transition-shadow">
40              <div className="relative h-48">
41                <Image
42                  src={attraction.imageUrl}
43                  alt={attraction.name}
44                  fill
45                  className="object-cover transition-transform group-hover:scale-105"
46                />
47              </div>
48              <div className="p-4">
49                <h2 className="text-xl font-semibold mb-2">{attraction.name}</h2>
50                <p className="text-gray-600 line-clamp-2">{attraction.shortDescription}</p>
51              </div>
52            </div>
53          </Link>
54        ))}
55      </div>
56    </div>
57  );
58}
1// app/destinations/[destinationId]/attractions/[attractionId]/page.tsx
2import { getDestinationById, getAttractionById } from '@/lib/api';
3import Link from 'next/link';
4import Image from 'next/image';
5import { notFound } from 'next/navigation';
6
7export default async function AttractionPage({ 
8  params 
9}: { 
10  params: Promise<{ destinationId: string; attractionId: string }> 
11}) {
12  const { destinationId, attractionId } = await params;
13  
14  const [destination, attraction] = await Promise.all([
15    getDestinationById(destinationId),
16    getAttractionById(destinationId, attractionId)
17  ]);
18  
19  if (!destination || !attraction) {
20    notFound();
21  }
22  
23  return (
24    <div className="container mx-auto px-4 py-12">
25      <div className="flex items-center gap-2 mb-8 text-sm text-gray-500">
26        <Link href="/destinations" className="hover:text-indigo-600">Destynacje</Link>
27        <span>/</span>
28        <Link href={`/destinations/\${destinationId}\`} className="hover:text-indigo-600">
29          {destination.name}
30        </Link>
31        <span>/</span>
32        <Link href={`/destinations/\${destinationId}/attractions\`} className="hover:text-indigo-600">
33          Atrakcje
34        </Link>
35        <span>/</span>
36        <span>{attraction.name}</span>
37      </div>
38      
39      <div className="grid grid-cols-1 lg:grid-cols-2 gap-12">
40        <div className="relative h-[400px] lg:h-[600px] rounded-lg overflow-hidden">
41          <Image
42            src={attraction.imageUrl}
43            alt={attraction.name}
44            fill
45            className="object-cover"
46            priority
47          />
48        </div>
49        
50        <div>
51          <h1 className="text-4xl font-bold mb-4">{attraction.name}</h1>
52          <div className="flex items-center mb-6">
53            <span className="px-3 py-1 bg-indigo-100 text-indigo-800 rounded-full text-sm">
54              {attraction.category}
55            </span>
56            <span className="mx-4 text-gray-300">|</span>
57            <div className="flex items-center text-amber-500">
58              {/* Gwiazdki oceny */}
59              {Array.from({ length: 5 }).map((_, i) => (
60                <svg key={i} xmlns="http://www.w3.org/2000/svg" className={`h-5 w-5 \${i < attraction.rating ? 'fill-current' : 'text-gray-300'}\`} viewBox="0 0 20 20" fill="currentColor">
61                  <path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
62                </svg>
63              ))}
64              <span className="ml-2 text-gray-700">{attraction.rating.toFixed(1)}</span>
65            </div>
66          </div>
67          
68          <p className="text-gray-700 mb-6">{attraction.description}</p>
69          
70          <div className="space-y-4 mb-8">
71            <div className="flex">
72              <div className="w-32 font-semibold">Lokalizacja:</div>
73              <div>{attraction.location}</div>
74            </div>
75            <div className="flex">
76              <div className="w-32 font-semibold">Koszt zwiedzania:</div>
77              <div>{attraction.price} kredytów</div>
78            </div>
79            <div className="flex">
80              <div className="w-32 font-semibold">Czas zwiedzania:</div>
81              <div>{attraction.duration}</div>
82            </div>
83          </div>
84          
85          <button className="w-full py-3 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors">
86            Zarezerwuj wizytę
87          </button>
88        </div>
89      </div>
90    </div>
91  );
92}

Parametry catch-all

W Metropolii Quantum, technologia Q-Route umożliwia podróżowanie do dowolnej liczby miejsc na podstawie jednego kodu trasy. Podobnie, w Next.js 15, parametry catch-all umożliwiają obsługę ścieżek z dowolną liczbą segmentów.

Składnia parametrów catch-all

Parametry catch-all są oznaczane trzema kropkami przed nazwą parametru w nawiasach kwadratowych:

1[...param]

Ta notacja pozwala na dopasowanie dowolnej liczby segmentów URL, które zostaną przekazane jako tablica do komponentu.

Struktura katalogów

1app/
2  ├── shop/                   # /shop
3  │   └── [...categories]/    # /shop/category1
4  │       └── page.tsx        # /shop/category1/category2/category3/etc.

W tym przykładzie, ścieżka

/shop/clothing/men/shirts
zostanie dopasowana przez
[...categories]
i parametr
categories
będzie zawierał tablicę
['clothing', 'men', 'shirts']
.

Implementacja dla Quantum Voyages

Implementacja systemu kategorii produktów z parametrami catch-all:

1// app/shop/[...categories]/page.tsx
2import { fetchCategorizedProducts } from '@/lib/api/products';
3import { Breadcrumbs } from '@/components/shop/Breadcrumbs';
4import { ProductGrid } from '@/components/shop/ProductGrid';
5import { CategoryFilters } from '@/components/shop/CategoryFilters';
6
7export default async function CategoriesPage({ params }: { params: Promise<{ categories: string[] }> }) {
8  const { categories } = await params;
9  
10  // Pobieranie produktów na podstawie wszystkich przekazanych kategorii
11  const { products, availableFilters } = await fetchCategorizedProducts(categories);
12  
13  // Konstruowanie tytułu na podstawie ostatniej kategorii
14  const currentCategory = categories[categories.length - 1];
15  const formattedCategoryName = currentCategory
16    .split('-')
17    .map(word => word.charAt(0).toUpperCase() + word.slice(1))
18    .join(' ');
19  
20  return (
21    <div className="container mx-auto px-4 py-12">
22      <Breadcrumbs categories={categories} />
23      
24      <h1 className="text-3xl font-bold mb-8">{formattedCategoryName}</h1>
25      
26      <div className="grid grid-cols-1 lg:grid-cols-4 gap-8">
27        <div className="lg:col-span-1">
28          <CategoryFilters 
29            categories={categories} 
30            availableFilters={availableFilters} 
31          />
32        </div>
33        
34        <div className="lg:col-span-3">
35          <ProductGrid products={products} />
36        </div>
37      </div>
38    </div>
39  );
40}

Komponenty pomocnicze

1// components/shop/Breadcrumbs.tsx
2import Link from 'next/link';
3
4export function Breadcrumbs({ categories }: { categories: string[] }) {
5  return (
6    <div className="flex items-center flex-wrap gap-2 mb-8 text-sm text-gray-500">
7      <Link href="/shop" className="hover:text-indigo-600">
8        Sklep
9      </Link>
10      
11      {categories.map((category, index) => {
12        // Budowanie ścieżki URL dla tej kategorii
13        const url = `/shop/\${categories.slice(0, index + 1).join('/')}`;
14        const isLast = index === categories.length - 1;
15        
16        const formattedName = category
17          .split('-')
18          .map(word => word.charAt(0).toUpperCase() + word.slice(1))
19          .join(' ');
20        
21        return (
22          <div key={category} className="flex items-center">
23            <span className="mx-2">/</span>
24            {isLast ? (
25              <span>{formattedName}</span>
26            ) : (
27              <Link href={url} className="hover:text-indigo-600">
28                {formattedName}
29              </Link>
30            )}
31          </div>
32        );
33      })}
34    </div>
35  );
36}

Opcjonalne parametry catch-all

W Next.js 15 możemy też zdefiniować opcjonalne parametry catch-all, używając podwójnych nawiasów kwadratowych:

1[[...param]]

Główna różnica polega na tym, że opcjonalny parametr catch-all dopasuje też ścieżkę bez żadnych segmentów. Na przykład,

app/shop/[[...categories]]/page.tsx
dopasuje:

  • /shop
    (w tym przypadku
    categories
    będzie pustą tablicą
    []
    )
  • /shop/clothing
    (
    categories
    =
    ['clothing']
    )
  • /shop/clothing/men/shirts
    (
    categories
    =
    ['clothing', 'men', 'shirts']
    )

Implementacja sklepu z opcjonalnymi parametrami catch-all

1// app/shop/[[...categories]]/page.tsx
2import { fetchCategorizedProducts, fetchFeaturedProducts } from '@/lib/api/products';
3import { Breadcrumbs } from '@/components/shop/Breadcrumbs';
4import { ProductGrid } from '@/components/shop/ProductGrid';
5import { CategoryFilters } from '@/components/shop/CategoryFilters';
6import { FeaturedProducts } from '@/components/shop/FeaturedProducts';
7import { CategoryPreview } from '@/components/shop/CategoryPreview';
8
9export default async function ShopPage({ params }: { params: Promise<{ categories?: string[] }> }) {
10  const categories = (await params).categories || [];
11  const isRootShopPage = categories.length === 0;
12  
13  if (isRootShopPage) {
14    // Jesteśmy na głównej stronie sklepu
15    const featuredProducts = await fetchFeaturedProducts();
16    const topCategories = await fetchTopCategories();
17    
18    return (
19      <div className="container mx-auto px-4 py-12">
20        <h1 className="text-4xl font-bold mb-12">Sklep Quantum Voyages</h1>
21        
22        <section className="mb-16">
23          <h2 className="text-2xl font-semibold mb-8">Polecane produkty</h2>
24          <FeaturedProducts products={featuredProducts} />
25        </section>
26        
27        <section>
28          <h2 className="text-2xl font-semibold mb-8">Przeglądaj kategorie</h2>
29          <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
30            {topCategories.map(category => (
31              <CategoryPreview key={category.id} category={category} />
32            ))}
33          </div>
34        </section>
35      </div>
36    );
37  } else {
38    // Jesteśmy na stronie kategorii
39    const { products, availableFilters } = await fetchCategorizedProducts(categories);
40    
41    // Konstruowanie tytułu na podstawie ostatniej kategorii
42    const currentCategory = categories[categories.length - 1];
43    const formattedCategoryName = currentCategory
44      .split('-')
45      .map(word => word.charAt(0).toUpperCase() + word.slice(1))
46      .join(' ');
47    
48    return (
49      <div className="container mx-auto px-4 py-12">
50        <Breadcrumbs categories={categories} />
51        
52        <h1 className="text-3xl font-bold mb-8">{formattedCategoryName}</h1>
53        
54        <div className="grid grid-cols-1 lg:grid-cols-4 gap-8">
55          <div className="lg:col-span-1">
56            <CategoryFilters 
57              categories={categories} 
58              availableFilters={availableFilters} 
59            />
60          </div>
61          
62          <div className="lg:col-span-3">
63            <ProductGrid products={products} />
64          </div>
65        </div>
66      </div>
67    );
68  }
69}

Dostęp do parametrów w komponentach

W Next.js 15, możemy uzyskać dostęp do parametrów ścieżki na kilka sposobów:

W komponentach po stronie serwera (Server Components)

1// Server Component
2export default async function ProductPage({ params }: { params: Promise<{ productId: string }> }) {
3  // Użycie (await params).productId
4  return <div>Product ID: {(await params).productId}</div>;
5}

W komponentach po stronie klienta (Client Components)

W komponentach po stronie klienta, możemy używać hooka

useParams
z
next/navigation
:

1'use client';
2
3import { useParams } from 'next/navigation';
4
5export default function ClientProductDetails() {
6  const params = useParams();
7  const productId = params.productId;
8  
9  return <div>Viewing product: {productId}</div>;
10}

W funkcjach generowania

Next.js 15 oferuje funkcje do generowania statycznych parametrów i metadanych:

1// Generowanie statycznych parametrów
2export async function generateStaticParams() {
3  const products = await fetchTopProducts();
4  
5  return products.map((product) => ({
6    productId: product.id,
7  }));
8}
9
10// Generowanie metadanych
11export async function generateMetadata({ params }: { params: Promise<{ productId: string }> }) {
12  const product = await fetchProductById((await params).productId);
13  
14  return {
15    title: product.name,
16    description: product.description,
17    openGraph: {
18      images: [{ url: product.imageUrl }],
19    },
20  };
21}

Walidacja i obsługa parametrów

Podczas pracy z parametrami dynamicznymi, ważne jest, aby przeprowadzić ich walidację:

1export default async function ProductPage({ params }: { params: Promise<{ productId: string }> }) {
2  // Sprawdź, czy productId jest poprawnym UUID
3  if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test((await params).productId)) {
4    // Wyrzuć błąd, który zostanie przechwycony przez najbliższy error.js
5    throw new Error('Invalid product ID format');
6  }
7  
8  const product = await fetchProductById((await params).productId);
9  
10  // Jeśli produkt nie istnieje, wyświetl stronę 404
11  if (!product) {
12    notFound();
13  }
14  
15  return <ProductDetails product={product} />;
16}

Obsługa błędów i brakujących zasobów

Next.js 15 umożliwia eleganckie obsługiwanie sytuacji, gdy zasób nie zostanie znaleziony lub wystąpi błąd:

Obsługa błędów z error.js

1// app/products/[productId]/error.tsx
2'use client';
3
4import { useEffect } from 'react';
5
6export default function ProductError({
7  error,
8  reset,
9}: {
10  error: Error & { digest?: string };
11  reset: () => void;
12}) {
13  useEffect(() => {
14    // Możesz zalogować błąd do zewnętrznego serwisu
15    console.error(error);
16  }, [error]);
17
18  return (
19    <div className="container mx-auto px-4 py-16 text-center">
20      <h2 className="text-2xl font-bold mb-4">Coś poszło nie tak!</h2>
21      <p className="mb-8 text-gray-600">{error.message}</p>
22      <button
23        onClick={reset}
24        className="px-4 py-2 bg-indigo-600 text-white rounded hover:bg-indigo-700"
25      >
26        Spróbuj ponownie
27      </button>
28    </div>
29  );
30}

Obsługa brakujących zasobów z not-found.js

1// app/products/[productId]/not-found.tsx
2import Link from 'next/link';
3
4export default function ProductNotFound() {
5  return (
6    <div className="container mx-auto px-4 py-16 text-center">
7      <h2 className="text-3xl font-bold mb-4">Produkt nie znaleziony</h2>
8      <p className="mb-8 text-gray-600">
9        Przepraszamy, nie mogliśmy znaleźć szukanego produktu.
10      </p>
11      <Link
12        href="/products"
13        className="px-4 py-2 bg-indigo-600 text-white rounded hover:bg-indigo-700"
14      >
15        Wróć do wszystkich produktów
16      </Link>
17    </div>
18  );
19}

Przykład: zaawansowany system dokumentacji

W Metropolii Quantum, interaktywny system dokumentacji pozwala na nawigację przez złożone hierarchie informacji. Zaimplementujmy podobny system w Next.js 15:

1app/
2  ├── docs/
3  │   └── [[...slug]]/     # Opcjonalny catch-all
4  │       └── page.tsx     # /docs, /docs/guides, /docs/guides/routing, etc.
1// app/docs/[[...slug]]/page.tsx
2import { fetchDocumentBySlug, fetchDocCategories } from '@/lib/api/documentation';
3import { DocSidebar } from '@/components/docs/DocSidebar';
4import { DocContent } from '@/components/docs/DocContent';
5import { DocBreadcrumbs } from '@/components/docs/DocBreadcrumbs';
6import { DocPagination } from '@/components/docs/DocPagination';
7import { notFound } from 'next/navigation';
8
9export default async function DocPage({ params }: { params: Promise<{ slug?: string[] }> }) {
10  const slug = (await params).slug || [];
11  const slugPath = slug.join('/');
12  
13  // Pobierz kategorie dokumentacji do menu bocznego
14  const categories = await fetchDocCategories();
15  
16  // Jeśli nie podano ścieżki, wyświetl stronę główną dokumentacji
17  if (slug.length === 0) {
18    return (
19      <div className="container mx-auto px-4 py-12">
20        <div className="flex flex-col lg:flex-row gap-8">
21          <DocSidebar categories={categories} currentSlug="" />
22          
23          <main className="flex-1">
24            <h1 className="text-4xl font-bold mb-8">Dokumentacja Quantum Voyages</h1>
25            
26            <div className="prose prose-indigo max-w-none">
27              <p>
28                Witaj w centrum dokumentacji Quantum Voyages. Znajdziesz tutaj wszystkie informacje
29                potrzebne do zaplanowania i odbycia podróży kosmicznej, od podstawowych przewodników
30                po zaawansowane instrukcje techniczne.
31              </p>
32              
33              <h2>Popularne sekcje</h2>
34              
35              <div className="grid grid-cols-1 md:grid-cols-2 gap-6 not-prose">
36                {categories.map(category => (
37                  <div key={category.slug} className="border rounded-lg p-6 hover:shadow-md transition-shadow">
38                    <h3 className="text-xl font-semibold mb-2">{category.title}</h3>
39                    <p className="text-gray-600 mb-4">{category.description}</p>
40                    <a href={`/docs/\${category.slug}\`} className="text-indigo-600 hover:underline">
41                      Przeglądaj {category.title.toLowerCase()}42                    </a>
43                  </div>
44                ))}
45              </div>
46            </div>
47          </main>
48        </div>
49      </div>
50    );
51  }
52  
53  // Pobierz dokument na podstawie ścieżki
54  const document = await fetchDocumentBySlug(slugPath);
55  
56  // Jeśli dokument nie istnieje, wyświetl stronę 404
57  if (!document) {
58    notFound();
59  }
60  
61  // Pobierz poprzedni i następny dokument dla nawigacji
62  const { previousDoc, nextDoc } = await fetchAdjacentDocs(slugPath);
63  
64  return (
65    <div className="container mx-auto px-4 py-12">
66      <DocBreadcrumbs slug={slug} />
67      
68      <div className="flex flex-col lg:flex-row gap-8">
69        <DocSidebar categories={categories} currentSlug={slugPath} />
70        
71        <main className="flex-1">
72          <h1 className="text-4xl font-bold mb-8">{document.title}</h1>
73          
74          <DocContent content={document.content} />
75          
76          <DocPagination previousDoc={previousDoc} nextDoc={nextDoc} />
77        </main>
78      </div>
79    </div>
80  );
81}
82
83// Generowanie statycznych parametrów dla popularnych ścieżek dokumentacji
84export async function generateStaticParams() {
85  const popularDocs = await fetchPopularDocs();
86  
87  return popularDocs.map(doc => ({
88    slug: doc.slug.split('/'),
89  }));
90}

Łączenie parametrów catch-all z innymi funkcjami

Next.js 15 pozwala na łączenie parametrów catch-all z innymi zaawansowanymi funkcjami routingu:

1. Grupowanie tras z parametrami catch-all

1app/
2  ├── (dashboard)/
3  │   └── admin/
4  │       └── users/
5  │           └── [...roles]/    # /admin/users/admin/editor/etc.
6  │               └── page.tsx

2. Interceptowanie tras z parametrami catch-all

1// app/(shop)/products/[...categories]/(.)[productId]/page.tsx
2// Interceptuje /products/category1/category2/productId jako modal
3// podczas nawigacji z /products/category1/category2

3. Równoległe trasy z parametrami catch-all

1app/
2  ├── trips/
3  │   ├── page.tsx              # /trips
4  │   ├── @filters/
5  │   │   └── [...params]/      # Równoległa trasa dla filtrów
6  │   │       └── page.tsx
7  │   └── @map/
8  │       └── [...location]/    # Równoległa trasa dla mapy
9  │           └── page.tsx

Zaawansowane warianty nawigacji z parametrami

Nawigacja z parametrami dynamicznymi

1'use client';
2
3import { useRouter } from 'next/navigation';
4
5export function CategoryNavigator({ categories, subcategories }) {
6  const router = useRouter();
7  
8  const handleCategoryChange = (categoryId) => {
9    const selectedCategory = categories.find(c => c.id === categoryId);
10    router.push(\`/shop/\${selectedCategory.slug}`);
11  };
12  
13  const handleSubcategoryChange = (categoryId, subcategoryId) => {
14    const selectedCategory = categories.find(c => c.id === categoryId);
15    const selectedSubcategory = subcategories.find(sc => sc.id === subcategoryId);
16    router.push(\`/shop/\${selectedCategory.slug}/\${selectedSubcategory.slug}`);
17  };
18  
19  return (
20    <div className="space-y-4">
21      <div>
22        <label className="block mb-2 text-sm font-medium">Kategoria</label>
23        <select 
24          className="w-full p-2 border rounded"
25          onChange={(e) => handleCategoryChange(e.target.value)}
26        >
27          <option value="">Wybierz kategorię</option>
28          {categories.map(category => (
29            <option key={category.id} value={category.id}>
30              {category.name}
31            </option>
32          ))}
33        </select>
34      </div>
35      
36      <div>
37        <label className="block mb-2 text-sm font-medium">Podkategoria</label>
38        <select 
39          className="w-full p-2 border rounded"
40          onChange={(e) => handleSubcategoryChange(selectedCategory, e.target.value)}
41        >
42          <option value="">Wybierz podkategorię</option>
43          {/* Opcje podkategorii */}
44        </select>
45      </div>
46    </div>
47  );
48}

Wyszukiwanie zaawansowane z parametrami catch-all

1// app/search/[[...query]]/page.tsx
2import { searchProducts } from '@/lib/api/search';
3
4export default async function SearchPage({ params, searchParams }) {
5  const query = (await params).query || [];
6  const searchTerm = query.join(' ');
7  
8  // Pobieranie parametrów zapytania
9  const { category, sort, filters } = await searchParams;
10  
11  // Wykonanie wyszukiwania
12  const results = await searchProducts({
13    searchTerm,
14    category,
15    sort,
16    filters: filters ? JSON.parse(filters) : undefined
17  });
18  
19  return (
20    <div className="container mx-auto px-4 py-12">
21      <h1 className="text-3xl font-bold mb-8">
22        {searchTerm 
23          ? `Wyniki wyszukiwania dla: "\${searchTerm}"` 
24          : 'Wyszukiwanie produktów'
25        }
26      </h1>
27      
28      {/* Filtry wyszukiwania */}
29      <SearchFilters 
30        initialCategory={category}
31        initialSort={sort}
32        initialFilters={filters ? JSON.parse(filters) : {}}
33      />
34      
35      {/* Wyniki wyszukiwania */}
36      <SearchResults results={results} />
37    </div>
38  );
39}

Podsumowanie

Zagnieżdżone ścieżki dynamiczne i parametry catch-all w Next.js 15 oferują niezwykle elastyczny system routingu, który może obsłużyć praktycznie każdy scenariusz nawigacji w nowoczesnych aplikacjach webowych:

  1. Zagnieżdżone parametry dynamiczne umożliwiają tworzenie złożonych hierarchicznych struktur URL, takich jak

    /destinations/:destinationId/attractions/:attractionId
    .

  2. Parametry catch-all (

    [...param]
    ) pozwalają na dopasowanie dowolnej liczby segmentów URL, co jest idealne dla systemów dokumentacji, kategorii produktów i elastycznych interfejsów wyszukiwania.

  3. Opcjonalne parametry catch-all (

    [[...param]]
    ) dodatkowo obsługują przypadek braku segmentów, co umożliwia użycie tego samego komponentu dla ścieżki bazowej i wszystkich jej podścieżek.

  4. Obsługa błędów i brakujących zasobów dzięki plikom

    error.js
    i
    not-found.js
    zapewnia eleganckie doświadczenie użytkownika nawet w przypadku problemów.

  5. Walidacja parametrów zapobiega błędom i zapewnia bezpieczeństwo aplikacji.

Łącząc te techniki z funkcjami takimi jak generowanie statycznych parametrów (

generateStaticParams
), możemy tworzyć wydajne, dynamiczne i łatwe w utrzymaniu systemy nawigacji dla aplikacji o dowolnej skali i złożoności.

W następnym rozdziale, zgłębimy zaawansowane techniki pracy z middlewarem w Next.js 15, co pozwoli nam implementować takie funkcje jak autentykacja, kontrola dostępu i przekierowania na poziomie routingu.

Vai a CodeWorlds