How to Build an Intelligent 404 Page with Fuzzy Search Suggestions in Next.js 15

How to Build an Intelligent 404 Page with Fuzzy Search Suggestions in Next.js 15

8 min read

How to Build an Intelligent 404 Page with Fuzzy Search Suggestions in Next.js 15

Introduction

When users land on a non-existent page, they often get a simple 404 error with no guidance on where to go next. This creates a poor user experience and increases bounce rates. In this guide, I'll walk you through how to build an intelligent 404 page that suggests similar URLs using fuzzy search in Next.js 15.

The Problem with Traditional 404 Pages

Traditional 404 pages:

  • Show a generic "Page Not Found" message
  • Offer no guidance on where to go next
  • Lead to high bounce rates
  • Don't help users find what they were looking for

What We'll Build

We'll create a 404 page that:

  • Uses server-side generation to avoid server-intensive operations
  • Implements advanced fuzzy matching with Fuse.js
  • Generates suggestions on the client-side
  • Automatically updates based on the sitemap
  • Maintains proper 404 status codes
  • Takes advantage of Next.js 15's improved server components and routing

Step-by-Step Implementation

Step 1: Set Up the API Endpoint

First, create an API endpoint that serves all valid URLs from the sitemap:

// src/app/api/urls/route.ts
import { NextResponse } from 'next/server';
import { getAllUrls } from '@/app/sitemap';

export async function GET() {
    console.log('API: /api/urls endpoint called');

    try {
        console.log('API: Fetching all URLs from sitemap');
        const urls = await getAllUrls();
        console.log(`API: Successfully fetched ${urls.length} URLs`);

        // Log a sample of URLs for debugging
        if (urls.length > 0) {
            console.log('API: Sample URLs:', urls.slice(0, 5));
        }

        // Create response with caching headers
        const response = NextResponse.json({ urls });
        
        // Set cache headers - cache for 24 hours
        response.headers.set('Cache-Control', 'public, max-age=86400, s-maxage=86400');
        
        return response;
    } catch (error) {
        const errorMessage = error instanceof Error ? error.message : 'Unknown error';
        console.error('API: Error fetching URLs:', errorMessage);
        console.error('API: Error stack:', error instanceof Error ? error.stack : 'No stack trace');

        return NextResponse.json(
            { error: `Failed to fetch URLs: ${errorMessage}` },
            { status: 500 }
        );
    }
}

This endpoint reuses the existing getAllUrls() function from the sitemap, ensuring consistency across the application. The added caching headers significantly improve performance by allowing browsers and CDNs to cache the response for 24 hours, reducing server load and improving response times.

Step 2: Install and Implement Fuzzy Search

Next, install Fuse.js and create a utility for fuzzy search:

npm install fuse.js

Then create a utility file:

// src/lib/fuzzy-search.ts
import Fuse from 'fuse.js';

/**
 * Interface for URL search results
 */
export interface UrlSearchResult {
  url: string;
  score: number;
}

/**
 * Finds similar URLs using fuzzy search
 * @param currentPath - The current path that was not found
 * @param allUrls - Array of all valid URLs in the application
 * @param options - Optional configuration for the fuzzy search
 * @returns Array of similar URLs with their scores
 */
export function findSimilarUrls(
  currentPath: string,
  allUrls: string[],
  options: {
    threshold?: number;
    limit?: number;
  } = {}
): UrlSearchResult[] {
  // Default options
  const { threshold = 0.4, limit = 5 } = options;
  
  // Configure Fuse.js
  const fuseOptions = {
    includeScore: true,
    threshold,
    keys: ['url'],
    minMatchCharLength: 2,
  };
  
  // Create Fuse instance
  const fuse = new Fuse(
    allUrls.map(url => ({ url })),
    fuseOptions
  );
  
  // Perform the search
  const searchResults = fuse.search(currentPath);
  
  // Filter and sort results
  return searchResults
    .filter(result => result.score && result.score < threshold)
    .map(result => ({
      url: result.item.url,
      score: result.score || 0
    }))
    .sort((a, b) => a.score - b.score)
    .slice(0, limit);
}

Fuse.js provides powerful fuzzy search capabilities with configurable thresholds and scoring.

Step 3: Create the SimilarUrlSuggestions Component

Now, build a React component that fetches URLs and displays suggestions:

// src/app/components/SimilarUrlSuggestions.tsx
'use client';

import { useEffect, useState } from 'react';
import { findSimilarUrls, UrlSearchResult } from '@/lib/fuzzy-search';
import Link from 'next/link';
import { usePathname } from 'next/navigation';

export default function SimilarUrlSuggestions() {
    const [similarUrls, setSimilarUrls] = useState<UrlSearchResult[]>([]);
    const [isLoading, setIsLoading] = useState(true);
    const [error, setError] = useState<string | null>(null);
    const pathname = usePathname();

    useEffect(() => {
        console.log('SimilarUrlSuggestions mounted with pathname:', pathname);

        if (!pathname) {
            console.log('No pathname available, skipping URL suggestions');
            setIsLoading(false);
            return;
        }

        async function fetchSimilarUrls() {
            try {
                console.log('Fetching URLs from API...');
                // Fetch all valid URLs from the server
                const response = await fetch('/api/urls');
                console.log('API response status:', response.status);

                if (!response.ok) {
                    throw new Error(`Failed to fetch URLs: ${response.status} ${response.statusText}`);
                }

                const data = await response.json();
                console.log('Received URLs count:', data.urls?.length || 0);

                if (!data.urls || !Array.isArray(data.urls)) {
                    throw new Error('Invalid response format: urls is not an array');
                }

                // Find similar URLs using fuzzy search
                console.log('Finding similar URLs for:', pathname);
                const similar = findSimilarUrls(pathname, data.urls, {
                    threshold: 0.7, // Higher threshold means more lenient matching (0.0 to 1.0)
                    limit: 8 // Show more suggestions
                });
                console.log('Found similar URLs:', similar);

                setSimilarUrls(similar);
            } catch (error) {
                const errorMessage = error instanceof Error ? error.message : 'Unknown error';
                console.error('Error finding similar URLs:', errorMessage);
                setError(errorMessage);
            } finally {
                setIsLoading(false);
            }
        }

        fetchSimilarUrls();
    }, [pathname]);

    if (isLoading) {
        console.log('Still loading similar URLs...');
        return null;
    }

    if (error) {
        console.log('Error state:', error);
        return null;
    }

    if (similarUrls.length === 0) {
        console.log('No similar URLs found');
        return null;
    }

    console.log('Rendering similar URLs:', similarUrls);
    return (
        <div className="text-center w-full px-4">
            <h3 className="text-xl font-semibold mb-4 text-gray-700">You might be looking for:</h3>
            <ul className="space-y-3">
                {similarUrls.map((result) => (
                    <li key={result.url}>
                        <Link
                            href={result.url.replace('https://example.com.ai', '')}
                            className="text-primary hover:underline text-lg break-words inline-block"
                        >
                            {result.url.replace('https://example.com.ai', '')}
                        </Link>
                    </li>
                ))}
            </ul>
        </div>
    );
}

This component:

  • Uses the usePathname hook to get the current path
  • Fetches all valid URLs from the API
  • Uses Fuse.js to find similar URLs
  • Displays the suggestions with proper styling

Step 4: Integrate with the 404 Page

Finally, integrate the component into your 404 page:

// src/app/not-found.tsx
import Link from 'next/link';
import MainLayout from './components/MainLayout';
import { Metadata } from 'next';
import SimilarUrlSuggestions from './components/SimilarUrlSuggestions';
import { headers } from 'next/headers';

export const metadata: Metadata = {
    title: '404 - Page Not Found | example.com',
    description: 'The page you are looking for could not be found.',
    robots: {
        index: false,
        follow: true,
    },
};

export default async function NotFound() {
    // Get the current path from headers
    const headersList = headers();
    const currentPath = headersList.get('x-invoke-path') || '';

    console.log('404 page rendered with path:', currentPath);

    // Check if it's an asset path
    const isAssetPath = currentPath.match(/\.(jpg|jpeg|png|gif|svg|ico|css|js)$/i);
    console.log('Is asset path:', !!isAssetPath);

    if (isAssetPath) {
        console.log('Skipping similar URL suggestions for asset path');
    } else {
        console.log('Will show similar URL suggestions');
    }

    return (
        <MainLayout>
            <div className="py-12 md:py-16">
                <div className="min-h-[40vh]">
                    {/* Only show suggestions for non-asset paths */}
                    {!isAssetPath && (
                        <div className="mb-12">
                            <SimilarUrlSuggestions />
                        </div>
                    )}
                    
                    <div className="text-center">
                        <h1 className="text-6xl font-bold mb-4">404</h1>
                        <h2 className="text-2xl font-semibold mb-4">Page Not Found</h2>
                        <p className="text-gray-600 mb-8">
                            The page you are looking for could not be found.
                        </p>
                        <Link
                            href="/"
                            className="inline-flex items-center justify-center rounded-md bg-primary px-6 py-3 text-white hover:bg-primary/90"
                        >
                            Return Home
                        </Link>
                    </div>
                </div>
            </div>
        </MainLayout>
    );
}

The 404 page:

  • Gets the current path from headers
  • Checks if it's an asset path (to avoid showing suggestions for missing images, etc.)
  • Displays the suggestions at the top of the page
  • Shows a standard 404 message below

Key Benefits of This Approach

  1. Server-Side Generation: The API endpoint uses server-side generation to avoid server-intensive operations.

  2. Advanced Matching: Fuse.js provides sophisticated fuzzy search algorithms that can find similar URLs even with typos or partial matches.

  3. Client-Side Processing: The fuzzy search happens on the client side, reducing server load.

  4. Automatic Updates: The solution uses the sitemap, so it automatically updates when new pages are added.

  5. Proper 404 Status: The page still returns a 404 status code, which is important for search engines.

  6. Responsive Design: The suggestions are styled to work well on all devices, with proper word wrapping for long URLs.

  7. Next.js 15 Compatibility: This implementation takes advantage of Next.js 15's improved server components, routing system, and performance optimizations.

How to Tune the Fuzzy Search

You can tune the fuzzy search by adjusting the threshold:

  • Lower threshold (e.g., 0.3): More strict matching, fewer but more relevant results
  • Higher threshold (e.g., 0.7): More lenient matching, more but potentially less relevant results

For our implementation, we chose a threshold of 0.7 to show more suggestions, which is better for helping users find what they're looking for.

Conclusion

This solution provides a much better user experience for visitors who land on non-existent pages. By suggesting similar URLs, you help them find what they're looking for and reduce bounce rates. The implementation is efficient, maintainable, and automatically updates as your site grows.

The combination of server-side generation, client-side processing, and advanced fuzzy search creates a powerful solution that enhances the user experience without compromising performance or search engine visibility. This approach is fully compatible with Next.js 15 and leverages its latest features for optimal performance.

nextjsnextjs15reactweb developmentuser experiencefuzzy search404 page