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 15, 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 15.
W Next.js 15 możemy obsługiwać strony 404 na dwa główne sposoby:
Aby utworzyć globalną stronę 404 w Next.js 15, 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.
Next.js 15 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}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.
Możemy również dostosować metadane dla naszej strony 404, aby poprawić SEO i doświadczenie użytkownika:
1// app/not-found.tsx
2import { 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}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 15 oferuje kilka metod implementacji przekierowań:
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 tymczasoweRóżnica między trwałym a tymczasowym przekierowaniem:
Next.js 15 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.Middleware w Next.js pozwala na przechwytywanie żądań przed ich przetworzeniem przez aplikację, co daje możliwość implementacji bardziej złożonych logik przekierowań:
1// middleware.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4
5export function middleware(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 const country = request.geo?.country || 'US';
25 if (pathname === '/events' && country === 'PL') {
26 return NextResponse.redirect(new URL('/wydarzenia', request.url));
27 }
28
29 // Przekierowanie na podstawie preferencji językowych przeglądarki
30 const preferredLocale = request.headers.get('accept-language')?.split(',')[0].split('-')[0];
31 if (pathname === '/' && preferredLocale === 'pl' && !pathname.includes('/pl')) {
32 return NextResponse.redirect(new URL('/pl', request.url));
33 }
34
35 return NextResponse.next();
36}
37
38export const config = {
39 matcher: [
40 '/((?!api|_next/static|_next/image|favicon.ico).*)',
41 ],
42};Middleware daje nam ogromną elastyczność, umożliwiając przekierowania oparte na:
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}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}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}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}Możemy również zaimplementować przekierowania oparte na testach A/B, aby kierować użytkowników do różnych wersji stron:
1// middleware.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 middleware(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};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 try {
7 const status = await getSystemStatus();
8
9 // Jeśli system jest w trakcie konserwacji, przekieruj do strony informacyjnej
10 if (status.maintenance) {
11 redirect('/maintenance');
12 }
13
14 // Jeśli system wymaga aktualizacji klienta, przekieruj do strony aktualizacji
15 if (status.requiresClientUpdate) {
16 redirect('/update-required');
17 }
18
19 return (
20 <div className="quantum-dashboard">
21 <h1>Panel kontrolny</h1>
22 {/* Zawartość panelu */}
23 </div>
24 );
25 } catch (error) {
26 // Jeśli API jest niedostępne, przekieruj do strony statusu
27 redirect('/system-status');
28 }
29}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}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 { middleware } from '../middleware';
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 old-quantum-district to quantum-districts/central', async () => {
25 const req = new NextRequest(new URL(`http://localhost:\${port}/old-quantum-district`));
26 const res = middleware(req);
27
28 expect(res.status).toBe(307); // lub 308 dla permanent
29 expect(res.headers.get('Location')).toBe(`http://localhost:\${port}/quantum-districts/central`);
30 });
31
32 test('Should handle country-specific redirects', async () => {
33 const req = new NextRequest(new URL(`http://localhost:\${port}/events`));
34 Object.defineProperty(req, 'geo', {
35 value: { country: 'PL' },
36 writable: true
37 });
38
39 const res = middleware(req);
40
41 expect(res.status).toBe(307);
42 expect(res.headers.get('Location')).toBe(`http://localhost:\${port}/wydarzenia`);
43 });
44});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 15 oferuje wszechstronne narzędzia do implementacji:
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ępnym rozdziale, omówimy techniki optymalizacji wydajności w Next.js 15, aby nasze aplikacje nie tylko były przyjazne dla użytkownika, ale również działały z maksymalną prędkością, zapewniając płynne doświadczenie w każdej sytuacji.