Kurs Next.js · Moduł 2: Routing i layouty
Obsługa 404 i przekierowań (not-found.js i redirects)
W tej lekcji6
W rozległym mieście kwantowym Metropolis Quantum nawigacja staje się kluczowym elementem codziennego życia. Co jednak, gdy mieszkaniec lub turysta próbuje dotrzeć do nieistniejącej lokacji? System miejski musi elegancko obsłużyć taki przypadek - albo informując, że miejsce nie istnieje, albo przekierowując do alternatywnej lokalizacji.
Podobnie w aplikacjach webowych zbudowanych na Next.js 16 musimy zadbać o eleganckie obsłużenie sytuacji, gdy użytkownik próbuje uzyskać dostęp do nieistniejącej strony lub zasobu. Ten rozdział skupia się na tworzeniu niestandardowych stron 404 oraz implementacji przekierowań w Next.js 16.
Obsługa stron 404 (nieznalezionych)
W Next.js 16 możemy obsługiwać strony 404 na dwa główne sposoby:
- Globalna strona 404 dla całej aplikacji
- Niestandardowe strony 404 dla konkretnych segmentów lub dynamicznych parametrów
Globalna strona 404
Aby utworzyć globalną stronę 404 w Next.js 16, należy utworzyć plik not-found.js lub not-found.tsx w katalogu głównym app:
1// app/not-found.tsx
2import Link from 'next/link';
3
4export default function NotFound() {
5 return (
6 <div className="quantum-not-found">
7 <h1>404: Lokalizacja Kwantowa Nieznaleziona</h1>
8 <div className="quantum-hologram">
9 <div className="hologram-effect">404</div>
10 </div>
11 <p>
12 Przepraszamy, ale miejsce, którego szukasz nie istnieje w Metropolis Quantum.
13 Nasze kwantowe czujniki nie mogły zlokalizować żądanego celu w żadnym z dostępnych
14 wymiarów czasoprzestrzennych.
15 </p>
16 <p>
17 Możliwe przyczyny błędu:
18 </p>
19 <ul>
20 <li>Koordynaty kwantowe są nieprawidłowe</li>
21 <li>Lokalizacja istniała, ale została usunięta</li>
22 <li>Quantum fluktuacje tymczasowo zakłóciły dostęp</li>
23 </ul>
24 <div className="action-buttons">
25 <Link href="/" className="quantum-button primary">
26 Powrót do Centrum Kontroli
27 </Link>
28 <Link href="/map" className="quantum-button secondary">
29 Otwórz Mapę Kwantową
30 </Link>
31 </div>
32 </div>
33 );
34}Ta strona będzie wyświetlana automatycznie, gdy użytkownik spróbuje uzyskać dostęp do nieistniejącej ścieżki URL.
Niestandardowe strony 404 dla konkretnych segmentów
Next.js 16 pozwala również na tworzenie niestandardowych stron 404 dla konkretnych segmentów aplikacji. Możesz utworzyć plik not-found.js lub not-found.tsx w dowolnym katalogu, a będzie on używany dla tego konkretnego segmentu i jego podścieżek:
1// app/quantum-properties/not-found.tsx
2import Link from 'next/link';
3
4export default function QuantumPropertiesNotFound() {
5 return (
6 <div className="property-not-found">
7 <h1>Posiadłość Kwantowa Nieznaleziona</h1>
8 <p>
9 Niestety, nie znaleźliśmy poszukiwanej przez Ciebie nieruchomości kwantowej.
10 Nasze sensory nie wykryły sygnatur kwantowych w podanej lokalizacji.
11 </p>
12 <div className="property-suggestions">
13 <h2>Sprawdź nasze najpopularniejsze nieruchomości:</h2>
14 <div className="suggestion-list">
15 {/* Lista popularnych nieruchomości */}
16 </div>
17 </div>
18 <Link href="/quantum-properties" className="quantum-button">
19 Wróć do wyszukiwarki nieruchomości
20 </Link>
21 </div>
22 );
23}Wywołanie not-found z poziomu komponentu
Możemy również programowo wywołać wyświetlenie strony 404 z dowolnego komponentu lub funkcji ładującej dane, używając funkcji notFound z next/navigation:
1// app/quantum-properties/[id]/page.tsx
2import { notFound } from 'next/navigation';
3import { getPropertyById } from '@/lib/api';
4
5export default async function QuantumPropertyPage({ params }: { params: Promise<{ id: string }> }) {
6 const property = await getPropertyById((await params).id);
7
8 // Jeśli nieruchomość nie istnieje, wyświetl stronę 404
9 if (!property) {
10 notFound();
11 }
12
13 return (
14 <div className="quantum-property-details">
15 <h1>{property.name}</h1>
16 {/* Szczegóły nieruchomości */}
17 </div>
18 );
19}Jest to szczególnie przydatne przy dynamicznych stronach, gdzie chcemy sprawdzić, czy dany zasób istnieje, a jeśli nie - wyświetlić stronę 404.
Generowanie metadanych dla strony 404
Możemy również dostosować metadane dla naszej strony 404, aby poprawić SEO i doświadczenie użytkownika:
1// app/not-found.tsx
2import type { Metadata } from 'next';
3import Link from 'next/link';
4
5export const metadata: Metadata = {
6 title: 'Strona nie znaleziona | Metropolis Quantum',
7 description: 'Nie mogliśmy znaleźć strony, której szukasz w Metropolis Quantum.',
8};
9
10export default function NotFound() {
11 // Zawartość strony 404
12}Przekierowania w Next.js 16
W Metropolis Quantum, gdy stara lokalizacja zostaje przeniesiona lub zastąpiona, system transportu automatycznie przekierowuje podróżnych do nowej destynacji. W Next.js podobnie używamy przekierowań, aby skierować użytkowników z nieaktualnych URL-i do nowych lokalizacji.
Next.js 16 oferuje kilka metod implementacji przekierowań:
- Statyczne przekierowania w konfiguracji
- Dynamiczne przekierowania w kodzie
- Tymczasowe vs. permanentne przekierowania
Statyczne przekierowania w next.config.js
Najprostszym sposobem zdefiniowania przekierowań jest użycie pliku konfiguracyjnego next.config.js:
1// next.config.js
2module.exports = {
3 async redirects() {
4 return [
5 {
6 // Przekierowanie z dawnej ścieżki do nowej
7 source: '/old-quantum-district',
8 destination: '/quantum-districts/central',
9 permanent: true, // kod statusu 308 (permanent redirect)
10 },
11 {
12 // Przekierowanie z parametrami
13 source: '/quantum-labs/:labId',
14 destination: '/research-facilities/:labId',
15 permanent: false, // kod statusu 307 (temporary redirect)
16 },
17 {
18 // Przekierowanie z zachowaniem parametrów zapytania
19 source: '/search-old',
20 destination: '/search-new',
21 permanent: false,
22 },
23 {
24 // Przekierowanie z wieloma parametrami i regex
25 source: '/quantum-zones/:zoneId(\\d{1,})',
26 destination: '/zones/:zoneId',
27 permanent: true,
28 },
29 ];
30 },
31};Każde przekierowanie zawiera:
source: ścieżka URL źródłowadestination: ścieżka URL docelowapermanent: flaga określająca, czy przekierowanie jest trwałe czy tymczasowe
Różnica między trwałym a tymczasowym przekierowaniem:
- Trwałe (permanent, kod 308): Informuje przeglądarki i boty wyszukiwarek, że strona została na stałe przeniesiona. Przeglądarki zapamiętują to przekierowanie i w przyszłości automatycznie przechodzą do nowego URL.
- Tymczasowe (temporary, kod 307): Wskazuje, że strona jest tymczasowo dostępna pod innym adresem, ale może wrócić pod oryginalnym URL. Przeglądarki nie zapamiętują tego przekierowania na dłużej.
Programowe przekierowania w kodzie
Next.js 16 umożliwia również dynamiczne przekierowania bezpośrednio w komponentach lub funkcjach danych za pomocą funkcji redirect z next/navigation:
1// app/quantum-profile/[userId]/page.tsx
2import { redirect } from 'next/navigation';
3import { getUserById } from '@/lib/api';
4
5export default async function UserProfilePage({ params }: { params: Promise<{ userId: string }> }) {
6 const user = await getUserById((await params).userId);
7
8 // Jeśli użytkownik nie istnieje, przekieruj na stronę logowania
9 if (!user) {
10 redirect('/login');
11 }
12
13 // Jeśli użytkownik jest administratorem, przekieruj na panel admina
14 if (user.role === 'admin') {
15 redirect('/admin/dashboard');
16 }
17
18 return (
19 <div className="user-profile">
20 <h1>Profil: {user.name}</h1>
21 {/* Zawartość profilu */}
22 </div>
23 );
24}Funkcja redirect jest szczególnie przydatna w sytuacjach, gdy decyzja o przekierowaniu zależy od danych dynamicznych, takich jak status użytkownika, uprawnienia, czy dostępność zasobów.
Przekierowania w proxy (dawniej middleware)
Proxy (w Next.js 15 i starszych: middleware) pozwala na przechwytywanie żądań przed ich przetworzeniem przez aplikację, co daje możliwość implementacji bardziej złożonych logik przekierowań:
1// proxy.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4
5export function proxy(request: NextRequest) {
6 // Pobieranie ścieżki z URL
7 const pathname = request.nextUrl.pathname;
8
9 // Przekierowanie warunkowe oparte na domenie
10 if (request.headers.get('host')?.includes('old-domain.com')) {
11 return NextResponse.redirect(
12 new URL(pathname, 'https://new-domain.com')
13 );
14 }
15
16 // Przekierowanie starych ścieżek do nowych
17 if (pathname.startsWith('/legacy-api')) {
18 return NextResponse.redirect(
19 new URL(pathname.replace('/legacy-api', '/api/v2'), request.url)
20 );
21 }
22
23 // Przekierowanie na podstawie geolokalizacji
24 // NextRequest nie ma już pola geo (usunięte w Next.js 15) - kraj podaje platforma, np. Vercel w nagłówku x-vercel-ip-country
25 const country = request.headers.get('x-vercel-ip-country') || 'US';
26 if (pathname === '/events' && country === 'PL') {
27 return NextResponse.redirect(new URL('/wydarzenia', request.url));
28 }
29
30 // Przekierowanie na podstawie preferencji językowych przeglądarki
31 const preferredLocale = request.headers.get('accept-language')?.split(',')[0].split('-')[0];
32 if (pathname === '/' && preferredLocale === 'pl' && !pathname.includes('/pl')) {
33 return NextResponse.redirect(new URL('/pl', request.url));
34 }
35
36 return NextResponse.next();
37}
38
39export const config = {
40 matcher: [
41 '/((?!api|_next/static|_next/image|favicon.ico).*)',
42 ],
43};Middleware daje nam ogromną elastyczność, umożliwiając przekierowania oparte na:
- Nagłówkach HTTP
- Cookies
- Geolokalizacji
- User-Agent
- Parametrach zapytania
- I wielu innych czynnikach
Przekierowania warunkowe w Server Components
W Server Components możemy używać przekierowań warunkowych w oparciu o dane pobierane z API lub bazy danych:
1// app/quantum-experiment/[experimentId]/page.tsx
2import { redirect } from 'next/navigation';
3import { getExperimentById, getUserPermissions } from '@/lib/api';
4
5export default async function ExperimentPage({ params }: { params: Promise<{ experimentId: string }> }) {
6 // Pobierz dane eksperymentu
7 const experiment = await getExperimentById((await params).experimentId);
8
9 // Jeśli eksperyment został przeniesiony, przekieruj do nowej lokalizacji
10 if (experiment?.movedTo) {
11 redirect(`/quantum-experiment/${experiment.movedTo}`);
12 }
13
14 // Jeśli eksperyment jest prywatny, sprawdź uprawnienia użytkownika
15 if (experiment?.isPrivate) {
16 const userPermissions = await getUserPermissions();
17
18 if (!userPermissions.canAccessPrivateExperiments) {
19 redirect('/access-denied');
20 }
21 }
22
23 return (
24 <div className="experiment-details">
25 <h1>{experiment.title}</h1>
26 {/* Szczegóły eksperymentu */}
27 </div>
28 );
29}Przekierowania w Client Components
W komponentach klienckich możemy również wykonywać przekierowania za pomocą hooków useRouter:
1'use client';
2
3import { useRouter } from 'next/navigation';
4import { useEffect } from 'react';
5import { useAuth } from '@/lib/auth';
6
7export default function ProtectedPage() {
8 const router = useRouter();
9 const { user, loading } = useAuth();
10
11 useEffect(() => {
12 // Przekieruj niezalogowanych użytkowników do strony logowania
13 if (!loading && !user) {
14 router.push('/login');
15 }
16 }, [user, loading, router]);
17
18 if (loading) {
19 return <div>Ładowanie...</div>;
20 }
21
22 if (!user) {
23 return null; // Nie renderuj nic podczas przekierowania
24 }
25
26 return (
27 <div className="protected-content">
28 <h1>Witaj, {user.name}!</h1>
29 {/* Chroniona zawartość strony */}
30 </div>
31 );
32}Zaawansowane techniki obsługi 404 i przekierowań
Obsługa nieistniejących parametrów dynamicznych
W przypadku tras dynamicznych z parametrami możemy generować statyczne strony tylko dla określonych parametrów, a dla pozostałych wyświetlać stronę 404:
1// app/quantum-experiments/[experimentId]/page.tsx
2import { notFound } from 'next/navigation';
3import { getExperimentById, getAllExperimentIds } from '@/lib/api';
4
5// Generowanie statycznych stron dla znanych eksperymentów
6export async function generateStaticParams() {
7 const experimentIds = await getAllExperimentIds();
8
9 return experimentIds.map((id) => ({
10 experimentId: id,
11 }));
12}
13
14export default async function ExperimentPage({ params }: { params: Promise<{ experimentId: string }> }) {
15 const experiment = await getExperimentById((await params).experimentId);
16
17 // Jeśli eksperyment nie istnieje, wyświetl 404
18 if (!experiment) {
19 notFound();
20 }
21
22 return (
23 <div className="experiment-details">
24 <h1>{experiment.title}</h1>
25 {/* Szczegóły eksperymentu */}
26 </div>
27 );
28}Obsługa wygasłych treści
Czasami treści mogą być tymczasowo dostępne lub wygasać po określonym czasie. Możemy obsłużyć taki przypadek, automatycznie przekierowując do alternatywnych stron:
1// app/quantum-events/[eventId]/page.tsx
2import { redirect, notFound } from 'next/navigation';
3import { getEventById } from '@/lib/api';
4
5export default async function EventPage({ params }: { params: Promise<{ eventId: string }> }) {
6 const event = await getEventById((await params).eventId);
7
8 // Jeśli wydarzenie nie istnieje, wyświetl 404
9 if (!event) {
10 notFound();
11 }
12
13 // Sprawdź, czy wydarzenie się już zakończyło
14 const now = new Date();
15 const eventEndDate = new Date(event.endDate);
16
17 if (now > eventEndDate) {
18 // Jeśli są dostępne nagrania, przekieruj do nich
19 if (event.recordings) {
20 redirect(`/quantum-events/recordings/${(await params).eventId}`);
21 }
22
23 // Jeśli są zaplanowane przyszłe wydarzenia z tej serii, przekieruj do nich
24 if (event.series && event.nextInSeries) {
25 redirect(`/quantum-events/${event.nextInSeries}`);
26 }
27
28 // W przeciwnym razie przekieruj do strony archiwum
29 redirect('/quantum-events/archive');
30 }
31
32 return (
33 <div className="event-details">
34 <h1>{event.title}</h1>
35 {/* Szczegóły wydarzenia */}
36 </div>
37 );
38}Niestandardowe przekierowania oparte na A/B testach
Możemy również zaimplementować przekierowania oparte na testach A/B, aby kierować użytkowników do różnych wersji stron:
1// proxy.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4
5// Funkcja pomocnicza do losowania wariantu testu A/B
6function getABTestVariant(userId: string, testName: string, variants: string[]): string {
7 // Używamy deterministycznego algorytmu na podstawie userId i nazwy testu
8 const hash = hashString(`${userId}-${testName}`);
9 const variantIndex = hash % variants.length;
10 return variants[variantIndex];
11}
12
13// Pomocnicza funkcja do generowania hasha ze stringa
14function hashString(str: string): number {
15 let hash = 0;
16 for (let i = 0; i < str.length; i++) {
17 hash = ((hash << 5) - hash) + str.charCodeAt(i);
18 hash |= 0; // Konwersja do 32-bitowej liczby całkowitej
19 }
20 return Math.abs(hash);
21}
22
23export function proxy(request: NextRequest) {
24 const pathname = request.nextUrl.pathname;
25
26 // A/B test dla strony głównej
27 if (pathname === '/') {
28 // Pobierz lub wygeneruj ID użytkownika
29 let userId = request.cookies.get('user_id')?.value;
30
31 if (!userId) {
32 userId = crypto.randomUUID();
33 // W prawdziwej implementacji należałoby zapisać to cookie w odpowiedzi
34 }
35
36 // Określ wariant testu A/B dla tego użytkownika
37 const homePageVariant = getABTestVariant(userId, 'homepage_redesign', ['control', 'variant_a', 'variant_b']);
38
39 // Przekieruj do odpowiedniego wariantu
40 if (homePageVariant !== 'control') {
41 return NextResponse.redirect(new URL(`/home-${homePageVariant}`, request.url));
42 }
43 }
44
45 return NextResponse.next();
46}
47
48export const config = {
49 matcher: ['/'],
50};Przekierowania bazujące na statusie API
Możemy wykonywać przekierowania na podstawie odpowiedzi z API:
1// app/quantum-dashboard/page.tsx
2import { redirect } from 'next/navigation';
3import { getSystemStatus } from '@/lib/api';
4
5export default async function DashboardPage() {
6 let status;
7 try {
8 status = await getSystemStatus();
9 } catch (error) {
10 // Jeśli API jest niedostępne, przekieruj do strony statusu
11 redirect('/system-status');
12 }
13
14 // redirect() rzuca specjalny błąd, dlatego wywołujemy go poza try/catch
15 if (status.maintenance) {
16 redirect('/maintenance');
17 }
18 if (status.requiresClientUpdate) {
19 redirect('/update-required');
20 }
21
22 return (
23 <div className="quantum-dashboard">
24 <h1>Panel kontrolny</h1>
25 {/* Zawartość panelu */}
26 </div>
27 );
28}Integracja z systemem analitycznym
W przypadku przekierowań i stron 404 warto śledzić, które ścieżki prowadzą do błędów lub przekierowań, aby optymalizować doświadczenie użytkownika. Oto przykład implementacji śledzenia dla stron 404:
1// app/not-found.tsx
2'use client';
3
4import { useEffect } from 'react';
5import Link from 'next/link';
6import { usePathname } from 'next/navigation';
7import { trackEvent } from '@/lib/analytics';
8
9export default function NotFound() {
10 const pathname = usePathname();
11
12 useEffect(() => {
13 // Śledzenie zdarzenia 404
14 trackEvent('404_error', {
15 path: pathname,
16 timestamp: new Date().toISOString(),
17 referrer: document.referrer
18 });
19 }, [pathname]);
20
21 return (
22 <div className="quantum-not-found">
23 <h1>404: Lokalizacja Kwantowa Nieznaleziona</h1>
24 {/* Zawartość strony 404 */}
25 </div>
26 );
27}Testowanie przekierowań i stron 404
Ważne jest, aby dokładnie przetestować przekierowania i strony 404, aby upewnić się, że działają zgodnie z oczekiwaniami:
1// tests/redirects.test.js
2import { createServer } from 'http';
3import { parse } from 'url';
4import fetch from 'node-fetch';
5import { NextRequest } from 'next/server';
6import { proxy } from '../proxy';
7
8describe('Redirects Tests', () => {
9 let server;
10 let port;
11
12 beforeAll(() => {
13 // Uruchom lokalny serwer testowy
14 server = createServer((req, res) => {
15 res.writeHead(200).end('Test server');
16 }).listen(0);
17 port = server.address().port;
18 });
19
20 afterAll(() => {
21 server.close();
22 });
23
24 test('Should redirect /legacy-api to /api/v2', async () => {
25 const req = new NextRequest(new URL(`http://localhost:${port}/legacy-api/users`));
26 const res = proxy(req);
27
28 expect(res.status).toBe(307);
29 expect(res.headers.get('Location')).toBe(`http://localhost:${port}/api/v2/users`);
30 });
31
32 test('Should handle country-specific redirects', async () => {
33 const req = new NextRequest(new URL(`http://localhost:${port}/events`), {
34 headers: { 'x-vercel-ip-country': 'PL' },
35 });
36
37 const res = proxy(req);
38
39 expect(res.status).toBe(307);
40 expect(res.headers.get('Location')).toBe(`http://localhost:${port}/wydarzenia`);
41 });
42});Podsumowanie
Prawidłowa obsługa stron 404 i przekierowań jest kluczowym elementem każdej profesjonalnej aplikacji Next.js. Podobnie jak w Metropolis Quantum, gdzie zaawansowane systemy nawigacji zapewniają, że każdy podróżny dotrze do właściwego miejsca (nawet jeśli pierwotna destynacja nie istnieje), tak i w naszej aplikacji musimy dbać o płynne doświadczenie użytkownika, nawet gdy napotyka on błędy.
Next.js 16 oferuje wszechstronne narzędzia do implementacji:
- Niestandardowych stron 404 globalnych i specyficznych dla segmentów
- Statycznych przekierowań w konfiguracji
- Dynamicznych przekierowań w kodzie
- Zaawansowanych reguł przekierowań w proxy
Dzięki tym narzędziom możemy tworzyć intuicyjne i przyjazne dla użytkownika aplikacje, które elegancko obsługują nawet nieoczekiwane sytuacje.
W następnej lekcji poznasz proxy (dawniej middleware), które przechwytuje żądania, zanim trafią do strony.
Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Co się dzieje z komponentem layout.tsx przy nawigacji między stronami, które współdzielą ten sam layout?
2. Czym różni się template.tsx od layout.tsx w Next.js App Router?
3. Plik not-found.js w App Routerze wyświetla się:
4. Intercepting Routes w App Routerze pozwalają na:
Zadania praktyczne w grze
- Edytor kodu
Stwórz w /app/dashboard/page.tsx: export default function Dashboard() { return <main className='dashboard-grid'><h1>Dashboard</h1><div className='lo-panel'>System Online</div></main> }. Wynik zobaczysz w podglądzie obok edytora.
- Układanie w poziomie
Ułóż import typu Metadata z biblioteki Next.js
- Edytor kodu
Stwórz /app/login/page.tsx z formularzem: 'use client' na samej górze (potrzebny useState), export default function LoginPage(), stan const [email, setEmail] = useState(''), w formularzu <input value={email} onChange={e => setEmail(e.target.value)} /> i przycisk „Zaloguj się do sieci”. Dodaj podstawowe style!
- Edytor kodu
Stwórz async Server Component (eksport domyślny): export default async function ImplantsDashboard() { const implants = await fetch('https://api.example.com/implants').then(r => r.json()); return <ul>{implants.map(i => <li key={i.id}>{i.name}</li>)}</ul> }. Na serwerze fetch potrzebuje pełnego adresu URL. Brak 'use client' = Server Component!
- Układanie w pionie
Ułóż kolejność renderowania komponentów w App Routerze
- Klikanie w kolejności
Ułóż składnię deklaracji obiektu metadata w Next.js
- Edytor kodu
W terminalu utwórz strukturę z Route Group (marketing) zawierającą strony about i contact: mkdir -p tworzy katalogi app/(marketing)/about i app/(marketing)/contact, a touch dodaje w każdym z nich plik page.tsx. Ścieżki z nawiasami weź w cudzysłów, np. mkdir -p "app/(marketing)/about".