A React component and utilities for suggesting similar routes when users hit 404 pages in Next.js applications using Levenshtein distance algorithm.
- 🔍 Automatic Route Discovery: Scans your Next.js App Router directory structure to find all valid routes
- 📏 Levenshtein Distance: Uses fast and accurate string similarity matching
- ⚛️ React Component: Drop-in component for displaying route suggestions
- 🎨 Customizable: Extensive styling and configuration options
- 🚀 TypeScript Support: Fully typed for better developer experience
- 📦 Lightweight: Minimal dependencies and optimized for performance
npm install route-hints
# or
yarn add route-hints
# or
pnpm add route-hintsYou can automatically discover routes from your Next.js app directory:
// scripts/generate-routes.js
const { generateRoutesFile } = require('route-hints');
// Generate routes.json file from your app directory
generateRoutesFile('./app', './public/routes.json');Add this to your package.json build process:
{
"scripts": {
"build": "node scripts/generate-routes.js && next build"
}
}// app/not-found.tsx
'use client';
import { useRouteSuggestions } from 'route-hints';
export default function NotFound() {
const { suggestions, isLoading, error, hasNoSuggestions } = useRouteSuggestions(
typeof window !== 'undefined' ? window.location.pathname : '/',
{
threshold: 3,
maxSuggestions: 5
}
);
return (
<div className="not-found-page">
<h1>404 - Page Not Found</h1>
<p>The page you're looking for doesn't exist.</p>
{isLoading && <p>Loading suggestions...</p>}
{error && <p>Error: {error}</p>}
{hasNoSuggestions && <p>No similar pages found.</p>}
{suggestions.length > 0 && (
<div>
<h3>Did you mean one of these pages?</h3>
<ul>
{suggestions.map((suggestion) => (
<li key={suggestion.path}>
<a href={suggestion.path}>
{suggestion.path}
</a>
<small> (similarity: {suggestion.distance})</small>
</li>
))}
</ul>
</div>
)}
</div>
);
}// app/not-found.tsx
import { RouteData } from 'route-hints';
export default function NotFound() {
return (
<div className="not-found-page">
<h1>404 - Page Not Found</h1>
<p>The page you're looking for doesn't exist.</p>
<RouteData
invalidPath={typeof window !== 'undefined' ? window.location.pathname : '/'}
threshold={3}
maxSuggestions={5}
>
{({ suggestions, isLoading, error, hasNoSuggestions }) => {
if (isLoading) return <p>Loading suggestions...</p>;
if (error) return <p>Error: {error}</p>;
if (hasNoSuggestions) return <p>No similar pages found.</p>;
return (
<div className="my-custom-suggestions">
<h3>Did you mean one of these pages?</h3>
<div className="suggestion-grid">
{suggestions.map((suggestion) => (
<div key={suggestion.path} className="suggestion-card">
<a href={suggestion.path} className="suggestion-link">
{suggestion.path}
</a>
<span className="similarity-score">
Score: {suggestion.distance}
</span>
</div>
))}
</div>
</div>
);
}}
</RouteData>
</div>
);
}For non-React usage or server-side logic:
import { getSuggestions, collectRoutes } from 'route-hints';
// Collect routes from your app directory
const routes = collectRoutes('./app');
// Get suggestions for an invalid path
const suggestions = getSuggestions('/prodcuts', routes, {
threshold: 2,
maxSuggestions: 3
});
console.log(suggestions);
// Output: [{ path: '/products', distance: 1 }]The main React hook for getting route suggestions data.
Parameters:
invalidPath: string- The current invalid path that triggered 404options: UseRouteSuggestionsOptions- Configuration options
Returns:
interface UseRouteSuggestionsReturn {
suggestions: RouteSuggestion[];
isLoading: boolean;
error: string | null;
hasNoSuggestions: boolean;
}Options:
interface UseRouteSuggestionsOptions {
routes?: string[]; // Optional: Custom list of valid routes
threshold?: number; // Default: 3 - Maximum Levenshtein distance
maxSuggestions?: number; // Default: 5 - Maximum number of suggestions
caseSensitive?: boolean; // Default: false - Case sensitive matching
autoLoad?: boolean; // Default: true - Auto-load suggestions
routesJsonPath?: string; // Default: '/routes.json' - Path to routes file
}A render prop component that provides suggestion data to its children.
Props:
interface RouteDataProps extends UseRouteSuggestionsOptions {
invalidPath: string;
children: (data: UseRouteSuggestionsReturn) => React.ReactNode;
}A pre-styled React component for displaying route suggestions. You can still use this if you want a quick implementation, but we recommend using the data hook or render prop component for full design control.
Scans a Next.js App Router directory and returns all valid route paths.
import { collectRoutes } from 'route-hints';
const routes = collectRoutes('./app');
console.log(routes); // ['/',' '/about', '/products', ...]Generates a JSON file containing all discovered routes.
import { generateRoutesFile } from 'route-hints';
generateRoutesFile('./app', './public/routes.json');getSuggestions(invalidPath: string, validRoutes: string[], options?: SuggestionOptions): RouteSuggestion[]
Returns all route suggestions within the specified threshold.
import { getSuggestions } from 'route-hints';
const suggestions = getSuggestions('/prodcts', ['/products', '/about'], {
threshold: 2,
caseSensitive: false
});
// Returns: [{ path: '/products', distance: 1 }]getBestSuggestions(invalidPath: string, validRoutes: string[], maxSuggestions?: number, threshold?: number): RouteSuggestion[]
Returns the best route suggestions up to the specified limit.
import { getBestSuggestions } from 'route-hints';
const bestSuggestions = getBestSuggestions('/prodcts', routes, 3, 2);Creates a configured route matcher with predefined settings.
import { createRouteMatcher } from 'route-hints';
const matcher = createRouteMatcher({
threshold: 2,
maxSuggestions: 5,
caseSensitive: false,
includeExactMatches: true,
includeStartsWithMatches: true
});
const suggestions = matcher.getSuggestions('/prodcts', routes);The component uses these CSS classes that you can style:
.route-suggestions {
/* Main container */
}
.route-suggestions__title {
/* Suggestion title */
}
.route-suggestions__list {
/* Suggestions list (ul) */
}
.route-suggestions__item {
/* Individual suggestion (li) */
}
.route-suggestions__link {
/* Suggestion links (a) */
}.route-suggestions {
padding: 1rem;
border: 1px solid #e1e5e9;
border-radius: 0.5rem;
background-color: #f8f9fa;
}
.route-suggestions__title {
color: #495057;
margin-bottom: 0.5rem;
font-size: 1.1rem;
}
.route-suggestions__list {
list-style: none;
padding: 0;
margin: 0;
}
.route-suggestions__item {
margin-bottom: 0.25rem;
}
.route-suggestions__link {
color: #007bff;
text-decoration: none;
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
display: inline-block;
transition: background-color 0.2s;
}
.route-suggestions__link:hover {
background-color: #e9ecef;
text-decoration: underline;
}The route discovery mechanism:
- Scans the App Router directory (
app/folder) - Identifies page files (
page.tsx,page.ts,page.js) - Builds URL paths based on folder structure
- Handles special cases:
- Route groups
(marketing)- don't affect URL structure - Private folders
_components- ignored - Dynamic routes
[id]- included as/[id]
- Route groups
app/
├── page.tsx # → /
├── about/
│ └── page.tsx # → /about
├── products/
│ ├── page.tsx # → /products
│ └── [id]/
│ └── page.tsx # → /products/[id]
├── (marketing)/
│ ├── pricing/
│ │ └── page.tsx # → /pricing
│ └── features/
│ └── page.tsx # → /features
└── _components/
└── Header.tsx # Ignored (private folder)
Generated routes: ["/", "/about", "/products", "/products/[id]", "/pricing", "/features"]
For better performance, generate routes at build time:
// next.config.js
const { generateRoutesFile } = require('route-hints');
/** @type {import('next').NextConfig} */
const nextConfig = {
webpack: (config, { isServer, dev }) => {
if (isServer && !dev) {
// Generate routes during production build
generateRoutesFile('./app', './public/routes.json');
}
return config;
},
};
module.exports = nextConfig;import { getSuggestions, calculateSimilarity } from 'route-hints';
function getSmartSuggestions(invalidPath: string, routes: string[]) {
// Use different thresholds based on path length
const threshold = invalidPath.length <= 5 ? 1 : Math.floor(invalidPath.length * 0.3);
return getSuggestions(invalidPath, routes, threshold);
}'use client';
import { SuggestedRoutes } from 'route-hints';
import { useEffect } from 'react';
export default function NotFound() {
useEffect(() => {
// Track 404 events
if (typeof window !== 'undefined') {
analytics.track('404_Page_View', {
path: window.location.pathname,
referrer: document.referrer
});
}
}, []);
return (
<div>
<h1>404 - Page Not Found</h1>
<SuggestedRoutes
invalidPath={typeof window !== 'undefined' ? window.location.pathname : '/'}
/>
</div>
);
}This package includes comprehensive TypeScript definitions:
import type {
RouteSuggestion,
SuggestedRoutesProps
} from 'route-hints';
const suggestion: RouteSuggestion = {
path: '/products',
distance: 1
};- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add some amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
MIT © [Your Name]
Made with ❤️ for the Next.js community