Wybór odpowiedniej architektury frontendowej decyduje o szybkości działania i łatwości skalowania e-commerce. W ekosystemie React to framework Next.js wyznacza standardy budowania wydajnych aplikacji. Kluczowym elementem tej technologii jest system zarządzania ścieżkami, który pozwala na intuicyjne projektowanie struktury adresów URL. Zrozumienie zasad działania routingu, zwłaszcza w kontekście nowoczesnego App Routera, pozwala tworzyć stabilne, zoptymalizowane pod kątem SEO i błyskawicznie ładujące się serwisy.
Czym jest file-system routing w Next.js?
Tradycyjne aplikacje React wymagają zewnętrznych bibliotek, takich jak React Router, do definiowania ścieżek i ręcznego utrzymywania konfiguracji. Next.js eliminuje ten problem, wprowadzając file-system routing. Oznacza to, że struktura adresów URL w aplikacji jest bezpośrednim odzwierciedleniem struktury katalogów na dysku.
W systemie tym każdy folder w katalogu głównym reprezentuje segment ścieżki. Aby dany adres mógł zostać wyrenderowany w przeglądarce, wewnątrz folderu musi znaleźć się plik page.js lub page.tsx. Dzięki temu deweloperzy mogą swobodnie umieszczać wewnątrz folderów inne pliki, takie jak komponenty pomocnicze, testy czy style, bez obawy o przypadkowe upublicznienie ich jako osobnych adresów.
Oto przykład podstawowej struktury katalogów w projekcie wykorzystującym katalog src/app, który warto poznać podczas konfiguracji projektu z Next.js 13:
src/
└── app/
├── page.tsx # Główny widok (/)
├── o-nas/
│ └── page.tsx # Ścieżka (/o-nas)
└── kontakt/
├── page.tsx # Ścieżka (/kontakt)
└── kontakt.css # Prywatny plik stylów
Taki układ sprawia, że zarządzanie architekturą informacji staje się niezwykle przejrzyste. Nowi programiści mogą natychmiast zorientować się w strukturze serwisu, po prostu przeglądając drzewo katalogów.
App Router vs Pages Router - ewolucja systemu ścieżek
Przez wiele lat standardem w Next.js był Pages Router, w którym każda ścieżka była definiowana przez plik w katalogu /pages. Rozwiązanie to niosło ze sobą ograniczenia architektoniczne. Trudno było w nim realizować zagnieżdżone układy graficzne (nested layouts) bez ponownego renderowania wspólnych elementów interfejsu. Dodatkowo, cała aplikacja opierała się na renderowaniu po stronie klienta lub klasycznym SSR, co zmuszało do przesyłania dużych ilości kodu JavaScript do przeglądarki.
Rozwiązaniem tych problemów stało się wprowadzenie App Routera, opartego na zmianach wprowadzonych w Next.js 13. Nowy system ścieżek, zlokalizowany w katalogu /app, całkowicie przedefiniował sposób budowania aplikacji.
Jego fundamentem są React Server Components w Next.js. Oznacza to, że komponenty są domyślnie renderowane po stronie serwera, co przekłada się na mniejszy rozmiar paczek kodu, lepszą wydajność oraz natychmiastowe ładowanie treści.
Główne różnice między oboma podejściami:
- Definiowanie ścieżek: W Pages Routerze ścieżką był plik (np. o-nas.tsx), w App Routerze jest to folder z plikiem page.tsx.
- Typ komponentów: W starszym routerze domyślnie Client Components, w nowym Server Components.
- Układy graficzne: Pages Router wymagał szablonów w _app.tsx, App Router oferuje natywne pliki layout.tsx.
- Pobieranie danych: Metody typu getServerSideProps zastąpiono standardowymi zapytaniami fetch w komponentach serwerowych.
Pliki specjalne w App Routerze i ich role
W App Routerze Next.js wykorzystuje zestaw plików o zastrzeżonych nazwach, które pozwalają na deklaratywne definiowanie zachowania interfejsu dla danego segmentu ścieżki. Każdy z tych plików pełni unikalną funkcję w hierarchii renderowania i pozwala na automatyczne wdrażanie zaawansowanych wzorców UX bez konieczności pisania skomplikowanej logiki stanowej.
layout.js vs template.js - zarządzanie wspólnym interfejsem i stanem
Plik layout.tsx służy do definiowania wspólnego układu graficznego dla wielu widoków w danym segmencie (np. stałego nagłówka, paska bocznego czy stopki). Kluczową cechą layoutu jest to, że zachowuje on swój stan i nie renderuje się ponownie, gdy użytkownik nawiguje między adresami należącymi do tego samego układu. Jeśli w layoucie znajduje się pole wyszukiwania z wpisanym tekstem, tekst ten nie zniknie po przejściu na inny adres w tym samym katalogu.
Plik template.tsx działa bardzo podobnie, ale różni się zachowaniem podczas nawigacji. Dla każdego adresu tworzy on całkowicie nową instancję komponentu. Oznacza to, że przy każdej zmianie adresu URL stan szablonu jest resetowany, a efekty uboczne (takie jak useEffect) są uruchamiane na nowo. Szablony są stosowane rzadziej niż layouty, ponieważ sprawdzają się głównie przy implementacji animacji wejścia lub w sytuacjach, gdy konieczne jest rejestrowanie odsłon w zewnętrznych narzędziach analitycznych przy każdym przejściu.
Obsługa ładowania i błędów - loading.js, error.js oraz not-found.js
Projektowanie nowoczesnych aplikacji wymaga dbałości o stany przejściowe oraz odporność na błędy sieciowe. Next.js dostarcza do tego dedykowane pliki specjalne, które automatycznie integrują się z mechanizmami Reacta:
- loading.tsx: Automatycznie opakowuje dany segment ścieżki w komponent React Suspense. Podczas pobierania danych użytkownik widzi stan ładowania, na przykład szkielet widoku (skeleton screen). Rozwiązanie to doskonale współgra z asynchronicznym pobieraniem danych w Next.js.
- error.tsx: Działa jako Error Boundary. Jeśli w danym segmencie dojdzie do błędu wykonania, aplikacja nie ulegnie całkowitej awarii. Zamiast tego wyrenderowany zostanie interfejs z pliku error.tsx (oznaczony dyrektywą 'use client'), oferujący np. przycisk ponownej próby.
- not-found.tsx: Definiuje dedykowany widok błędu 404 dla konkretnego segmentu lub globalnie dla całej aplikacji. Jest wywoływany, gdy żądany zasób nie istnieje lub gdy w kodzie wywołamy funkcję notFound().
Dynamiczny routing w praktyce - obsługa zmiennych parametrów URL
W projektach e-commerce adresy URL rzadko są statyczne. Karty produktów, artykuły blogowe czy kategorie wymagają obsługi zmiennych parametrów. Next.js realizuje to zadanie za pomocą dynamicznych segmentów (Dynamic Segments), które definiuje się poprzez umieszczenie nazwy folderu w nawiasach kwadratowych.
Istnieją trzy główne typy dynamicznych ścieżek:
- Podstawowy segment dynamiczny
[slug]: Dopasowuje pojedynczy, zmienny element ścieżki. Na przykład folder[id]dopasuje adres/uzytkownik/123. Parametr ten jest przekazywany do komponentu jako obiekt params.
- Segment typu Catch-all
[...slug]: Dopasowuje wszystkie kolejne, zagnieżdżone segmenty ścieżki. Folder[...kategoria]dopasuje zarówno/sklep/buty, jak i/sklep/buty/sportowe/meskie. W komponencie parametry te będą dostępne jako tablica ciągów znaków:['buty', 'sportowe', 'meskie'].
- Segment typu Optional Catch-all
[[...slug]]: Działa jak catch-all, ale dopasowuje również ścieżkę bazową bez dodatkowych parametrów. Folder[[...filtry]]dopasuje zarówno/katalog, jak i/katalog/rozmiar-m/kolor-czarny. Jest to przydatne przy budowaniu systemów filtrowania produktów.
Oto przykład implementacji dynamicznej karty produktu w TypeScript, pokazujący jak odebrać parametry z adresu URL:
interface ProductPageProps {
params: { slug: string };
}
export default function ProductPage({ params }: ProductPageProps) {
return (
<div className="product-container">
<h1>Produkt: {params.slug}</h1>
</div>
);
}
Nawigacja i wydajność - komponent Link oraz hook useRouter
Przechodzenie między adresami in Next.js zostało zoptymalizowane pod kątem maksymalnej wydajności i płynności działania. Podstawowym narzędziem do nawigacji deklaratywnej jest komponent <Link>, który rozszerza standardowy znacznik HTML <a>. Zamiast powodować pełne przeładowanie w przeglądarce, komponent ten przechwytuje kliknięcie i ładuje jedynie niezbędne dane dla nowego adresu, zachowując działanie aplikacji w trybie Single Page Application (SPA).
Jedną z najważniejszych funkcji komponentu <Link> jest automatyczny prefetching. Gdy tylko link pojawi się w polu widzenia użytkownika (viewport), Next.js automatycznie pobiera w tle kod i dane powiązanego adresu. Dzięki temu, w momencie gdy użytkownik faktycznie kliknie link, przejście następuje natychmiastowo, bez odczuwalnego opóźnienia. Mechanizm ten odgrywa kluczową rolę w procesie ogólnej optymalizacji wydajności strony, drastycznie poprawiając wskaźniki Core Web Vitals.
W sytuacjach, gdy nawigacja musi nastąpić programistycznie (na przykład po prawidłowym złożeniu zamówienia), wykorzystuje się hook useRouter importowany z pakietu next/navigation. Ponieważ hook ten działa po stronie klienta, komponent, w którym jest używany, musi zawierać dyrektywę 'use client' na samym początku pliku.
"use client";
import { useRouter } from "next/navigation";
export default function AddToCartButton() {
const router = useRouter();
return (
<button onClick={() => router.push("/koszyk")} className="btn-primary">
Przejdź do podsumowania
</button>
);
}
Routing Next.js w headless Shopify - mapowanie produktów i kolekcji
W architekturze headless e-commerce, gdzie Next.js odpowiada za warstwę prezentacji, a Shopify zarządza bazą danych i procesami sprzedażowymi, prawidłowe mapowanie ścieżek ma kluczowe znaczenie. Decydując się na zalety wdrożenia Shopify Headless, musimy odwzorować strukturę adresów URL tak, aby była przyjazna dla użytkowników oraz robotów wyszukiwarek.
Standardowy schemat adresacji w Shopify opiera się na strukturze /products/[handle] dla kart produktów oraz /collections/[handle] dla kategorii. W projekcie Next.js tworzymy odpowiadającą temu strukturę katalogów wewnątrz folderu src/app. Wykorzystując komponenty serwerowe, możemy pobierać dane bezpośrednio z Shopify Storefront API przy użyciu zapytań GraphQL, eliminując potrzebę stosowania dodatkowych bibliotek pośredniczących.
Aby zminimalizować czas ładowania witryny, warto zastosować funkcję generateStaticParams(). Pozwala ona na wygenerowanie statycznych ścieżek HTML dla najpopularniejszych produktów już na etapie budowania aplikacji (build time). Pozostałe, rzadziej odwiedzane produkty będą renderowane dynamicznie przy pierwszym zapytaniu i zapisywane w pamięci podręcznej.
Oto przykład komponentu serwerowego pobierającego dane produktu z Shopify na podstawie parametru handle:
import { notFound } from "next/navigation";
interface ProductProps {
params: { handle: string };
}
async function getShopifyProduct(handle: string) {
const res = await fetch("https://your-shop-name.myshopify.com/api/2024-04/graphql.json", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Shopify-Storefront-Access-Token": process.env.SHOPIFY_STOREFRONT_ACCESS_TOKEN || "",
},
body: JSON.stringify({
query: `query getProduct($handle: String!) { product(handle: $handle) { title description } }`,
variables: { handle },
}),
next: { revalidate: 3600 }
});
const { data } = await res.json();
return data?.product;
}
export default async function ShopifyProductPage({ params }: ProductProps) {
const product = await getShopifyProduct(params.handle);
if (!product) {
notFound();
}
return (
<main className="product-detail">
<h1>{product.title}</h1>
<p>{product.description}</p>
</main>
);
}
Precyzyjne połączenie elastycznego routingu Next.js z wydajnym API pozwala stworzyć sklep, który ładuje się błyskawicznie. Jeśli planujesz wdrożenie platformy sprzedażowej opartej na architekturze headless, profesjonalne wdrożenie sklepu Shopify z wykorzystaniem Next.js pozwoli w pełni wykorzystać potencjał obu tych technologii.
FAQ
Czy w App Routerze mogę nadal używać Pages Routera w tym samym projekcie?
Tak, Next.js w pełni wspiera współistnienie obu routerów w ramach jednej aplikacji. Pozwala to na stopniową, bezpieczną migrację starszych projektów do nowego standardu. Pamiętaj jednak, że w przypadku konfliktu nazw ścieżek, pierwszeństwo zawsze mają te zdefiniowane w App Routerze.
Jak działa prefetching w komponencie Link i czy można go wyłączyć?
Prefetching automatycznie pobiera w tle kod oraz dane powiązanego adresu, gdy tylko komponent Link pojawi się w polu widzenia użytkownika. Działa to domyślnie w środowisku produkcyjnym. Można wyłączyć to zachowanie, ustawiając atrybut prefetch={false} bezpośrednio w komponencie Link.
Czym różni się catch-all route od optional catch-all route?
Różnica dotyczy obsługi ścieżki bazowej. Klasyczny catch-all ([...slug]) wymaga podania przynajmniej jednego parametru w adresie URL, aby dopasować ścieżkę. Z kolei optional catch-all ([[...slug]]) dopasuje również adres bez żadnych parametrów, co jest idealne np. przy tworzeniu filtrów kategorii.
Jak obsłużyć błąd 404 dla produktu, którego nie ma w bazie Shopify?
Jeśli zapytanie do Shopify API nie zwróci danych dla podanego parametru handle, wewnątrz komponentu wywołuje się funkcję notFound() importowaną z pakietu 'next/navigation'. Spowoduje to natychmiastowe przerwanie renderowania i wyświetlenie najbliższego pliku not-found.tsx.
Czy pliki layout.js i template.js mogą współistnieć w jednym folderze?
Tak, mogą współistnieć w tym samym segmencie ścieżki. W takiej sytuacji Next.js renderuje szablon (template) wewnątrz układu (layout). Oznacza to, że layout opakowuje template, a template opakowuje właściwy komponent widoku (page).
Czym różni się obsługa błędów w error.js od global-error.js?
Plik error.js obsługuje błędy wewnątrz konkretnego segmentu ścieżki i jego dzieci. Nie potrafi jednak przechwycić błędów występujących w głównym, globalnym layoucie (src/app/layout.tsx). Do obsługi błędów na najwyższym poziomie aplikacji służy dedykowany plik global-error.js.