In the vast quantum city of Metropolis Quantum, navigation becomes a key element of daily life. But what happens when a resident or tourist tries to reach a non-existent location? The city system must elegantly handle such a case - either informing that the place doesn't exist, or redirecting to an alternative location.
Similarly, in web applications built with Next.js 15, we need to elegantly handle situations when a user tries to access a non-existent page or resource. This chapter focuses on creating custom 404 pages and implementing redirects in Next.js 15.
In Next.js 15, we can handle 404 pages in two main ways:
To create a global 404 page in Next.js 15, you need to create a
not-found.js or not-found.tsx file in the root app directory: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: Quantum Location Not Found</h1>
8 <div className="quantum-hologram">
9 <div className="hologram-effect">404</div>
10 </div>
11 <p>
12 We're sorry, but the place you're looking for doesn't exist in Metropolis Quantum.
13 Our quantum sensors could not locate the requested destination in any of the available
14 spacetime dimensions.
15 </p>
16 <p>
17 Possible causes of the error:
18 </p>
19 <ul>
20 <li>The quantum coordinates are incorrect</li>
21 <li>The location existed but has been removed</li>
22 <li>Quantum fluctuations temporarily disrupted access</li>
23 </ul>
24 <div className="action-buttons">
25 <Link href="/" className="quantum-button primary">
26 Return to Control Center
27 </Link>
28 <Link href="/map" className="quantum-button secondary">
29 Open Quantum Map
30 </Link>
31 </div>
32 </div>
33 );
34}This page will be displayed automatically when a user tries to access a non-existent URL path.
Next.js 15 also allows creating custom 404 pages for specific application segments. You can create a
not-found.js or not-found.tsx file in any directory, and it will be used for that specific segment and its sub-paths: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>Quantum Property Not Found</h1>
8 <p>
9 Unfortunately, we couldn't find the quantum property you're looking for.
10 Our sensors didn't detect quantum signatures at the specified location.
11 </p>
12 <div className="property-suggestions">
13 <h2>Check out our most popular properties:</h2>
14 <div className="suggestion-list">
15 {/* List of popular properties */}
16 </div>
17 </div>
18 <Link href="/quantum-properties" className="quantum-button">
19 Return to property search
20 </Link>
21 </div>
22 );
23}We can also programmatically trigger the display of a 404 page from any component or data loading function, using the
notFound function from 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 // If the property doesn't exist, display the 404 page
9 if (!property) {
10 notFound();
11 }
12
13 return (
14 <div className="quantum-property-details">
15 <h1>{property.name}</h1>
16 {/* Property details */}
17 </div>
18 );
19}This is particularly useful for dynamic pages where we want to check if a given resource exists, and if not - display the 404 page.
We can also customize metadata for our 404 page to improve SEO and user experience:
1// app/not-found.tsx
2import { Metadata } from 'next';
3import Link from 'next/link';
4
5export const metadata: Metadata = {
6 title: 'Page Not Found | Metropolis Quantum',
7 description: 'We could not find the page you are looking for in Metropolis Quantum.',
8};
9
10export default function NotFound() {
11 // 404 page content
12}In Metropolis Quantum, when an old location is moved or replaced, the transport system automatically redirects travelers to the new destination. In Next.js, we similarly use redirects to direct users from outdated URLs to new locations.
Next.js 15 offers several methods for implementing redirects:
The simplest way to define redirects is using the
next.config.js configuration file:1// next.config.js
2module.exports = {
3 async redirects() {
4 return [
5 {
6 // Redirect from old path to new one
7 source: '/old-quantum-district',
8 destination: '/quantum-districts/central',
9 permanent: true, // status code 308 (permanent redirect)
10 },
11 {
12 // Redirect with parameters
13 source: '/quantum-labs/:labId',
14 destination: '/research-facilities/:labId',
15 permanent: false, // status code 307 (temporary redirect)
16 },
17 {
18 // Redirect preserving query parameters
19 source: '/search-old',
20 destination: '/search-new',
21 permanent: false,
22 },
23 {
24 // Redirect with multiple parameters and regex
25 source: '/quantum-zones/:zoneId(\d{1,})',
26 destination: '/zones/:zoneId',
27 permanent: true,
28 },
29 ];
30 },
31};Each redirect contains:
source: the source URL pathdestination: the destination URL pathpermanent: a flag indicating whether the redirect is permanent or temporaryThe difference between permanent and temporary redirects:
Next.js 15 also enables dynamic redirects directly in components or data functions using the
redirect function from 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 // If the user doesn't exist, redirect to login page
9 if (!user) {
10 redirect('/login');
11 }
12
13 // If the user is an admin, redirect to admin dashboard
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 {/* Profile content */}
22 </div>
23 );
24}The
redirect function is particularly useful in situations where the redirect decision depends on dynamic data, such as user status, permissions, or resource availability.Middleware in Next.js allows intercepting requests before they are processed by the application, enabling the implementation of more complex redirect logic:
1// middleware.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4
5export function middleware(request: NextRequest) {
6 // Get path from URL
7 const pathname = request.nextUrl.pathname;
8
9 // Conditional redirect based on domain
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 // Redirect old paths to new ones
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 // Redirect based on geolocation
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 // Redirect based on browser language preferences
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 gives us enormous flexibility, enabling redirects based on:
In Server Components, we can use conditional redirects based on data fetched from the API or database:
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 // Fetch experiment data
7 const experiment = await getExperimentById((await params).experimentId);
8
9 // If the experiment has been moved, redirect to new location
10 if (experiment?.movedTo) {
11 redirect(`/quantum-experiment/\${experiment.movedTo}`);
12 }
13
14 // If the experiment is private, check user permissions
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 {/* Experiment details */}
27 </div>
28 );
29}In client components, we can also perform redirects using the
useRouter hook: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 // Redirect unauthenticated users to login page
13 if (!loading && !user) {
14 router.push('/login');
15 }
16 }, [user, loading, router]);
17
18 if (loading) {
19 return <div>Loading...</div>;
20 }
21
22 if (!user) {
23 return null; // Don't render anything during redirect
24 }
25
26 return (
27 <div className="protected-content">
28 <h1>Welcome, {user.name}!</h1>
29 {/* Protected page content */}
30 </div>
31 );
32}For dynamic routes with parameters, we can generate static pages only for specific parameters, and display a 404 page for the rest:
1// app/quantum-experiments/[experimentId]/page.tsx
2import { notFound } from 'next/navigation';
3import { getExperimentById, getAllExperimentIds } from '@/lib/api';
4
5// Generate static pages for known experiments
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 // If the experiment doesn't exist, display 404
18 if (!experiment) {
19 notFound();
20 }
21
22 return (
23 <div className="experiment-details">
24 <h1>{experiment.title}</h1>
25 {/* Experiment details */}
26 </div>
27 );
28}Sometimes content may be temporarily available or expire after a certain time. We can handle such cases by automatically redirecting to alternative pages:
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 // If the event doesn't exist, display 404
9 if (!event) {
10 notFound();
11 }
12
13 // Check if the event has already ended
14 const now = new Date();
15 const eventEndDate = new Date(event.endDate);
16
17 if (now > eventEndDate) {
18 // If recordings are available, redirect to them
19 if (event.recordings) {
20 redirect(`/quantum-events/recordings/\${(await params).eventId}`);
21 }
22
23 // If there are future events planned in this series, redirect to them
24 if (event.series && event.nextInSeries) {
25 redirect(`/quantum-events/\${event.nextInSeries}`);
26 }
27
28 // Otherwise redirect to the archive page
29 redirect('/quantum-events/archive');
30 }
31
32 return (
33 <div className="event-details">
34 <h1>{event.title}</h1>
35 {/* Event details */}
36 </div>
37 );
38}We can also implement redirects based on A/B tests to direct users to different page versions:
1// middleware.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4
5// Helper function to select A/B test variant
6function getABTestVariant(userId: string, testName: string, variants: string[]): string {
7 // We use a deterministic algorithm based on userId and test name
8 const hash = hashString(`\${userId}-\${testName}`);
9 const variantIndex = hash % variants.length;
10 return variants[variantIndex];
11}
12
13// Helper function to generate hash from string
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; // Convert to 32-bit integer
19 }
20 return Math.abs(hash);
21}
22
23export function middleware(request: NextRequest) {
24 const pathname = request.nextUrl.pathname;
25
26 // A/B test for the homepage
27 if (pathname === '/') {
28 // Get or generate user ID
29 let userId = request.cookies.get('user_id')?.value;
30
31 if (!userId) {
32 userId = crypto.randomUUID();
33 // In a real implementation, this cookie should be saved in the response
34 }
35
36 // Determine A/B test variant for this user
37 const homePageVariant = getABTestVariant(userId, 'homepage_redesign', ['control', 'variant_a', 'variant_b']);
38
39 // Redirect to the appropriate variant
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};We can perform redirects based on API responses:
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 // If the system is under maintenance, redirect to information page
10 if (status.maintenance) {
11 redirect('/maintenance');
12 }
13
14 // If the system requires a client update, redirect to update page
15 if (status.requiresClientUpdate) {
16 redirect('/update-required');
17 }
18
19 return (
20 <div className="quantum-dashboard">
21 <h1>Control Panel</h1>
22 {/* Panel content */}
23 </div>
24 );
25 } catch (error) {
26 // If the API is unavailable, redirect to status page
27 redirect('/system-status');
28 }
29}For redirects and 404 pages, it's worth tracking which paths lead to errors or redirects to optimize user experience. Here is an example of implementing tracking for 404 pages:
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 // Track 404 event
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: Quantum Location Not Found</h1>
24 {/* 404 page content */}
25 </div>
26 );
27}It's important to thoroughly test redirects and 404 pages to ensure they work as expected:
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 // Start local test server
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});Proper handling of 404 pages and redirects is a crucial element of every professional Next.js application. Just as in Metropolis Quantum, where advanced navigation systems ensure that every traveler reaches the right place (even if the original destination doesn't exist), we must also ensure a smooth user experience in our application, even when encountering errors.
Next.js 15 offers comprehensive tools for implementing:
With these tools, we can create intuitive and user-friendly applications that elegantly handle even unexpected situations.
In the next chapter, we will discuss performance optimization techniques in Next.js 15, so that our applications are not only user-friendly but also operate at maximum speed, ensuring a smooth experience in every situation.