Skip to content

mosabsomaer/route-hints

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

route-hints

A React component and utilities for suggesting similar routes when users hit 404 pages in Next.js applications using Levenshtein distance algorithm.

Features

  • 🔍 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

Installation

npm install route-hints
# or
yarn add route-hints
# or
pnpm add route-hints

Quick Start

1. Generate Routes (Optional)

You 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"
  }
}

2. Use the data hook (Recommended)

// 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>
  );
}

3. Alternative: Use the render prop component

// 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>
  );
}

4. Using Direct Functions (No React)

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 }]

API Reference

Hooks

useRouteSuggestions(invalidPath, options)

The main React hook for getting route suggestions data.

Parameters:

  • invalidPath: string - The current invalid path that triggered 404
  • options: 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
}

Components

RouteData

A render prop component that provides suggestion data to its children.

Props:

interface RouteDataProps extends UseRouteSuggestionsOptions {
  invalidPath: string;
  children: (data: UseRouteSuggestionsReturn) => React.ReactNode;
}

SuggestedRoutes (Optional styled component)

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.

Utilities

collectRoutes(appDirPath: string): string[]

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', ...]

generateRoutesFile(appDirPath: string, outputPath: string): void

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);

createRouteMatcher(config: RouteMatcherConfig)

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);

Styling

Default CSS Classes

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) */
}

Custom Styling Example

.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;
}

How Route Discovery Works

The route discovery mechanism:

  1. Scans the App Router directory (app/ folder)
  2. Identifies page files (page.tsx, page.ts, page.js)
  3. Builds URL paths based on folder structure
  4. Handles special cases:
    • Route groups (marketing) - don't affect URL structure
    • Private folders _components - ignored
    • Dynamic routes [id] - included as /[id]

Example Directory Structure

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"]

Advanced Usage

Server-Side Route Generation

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;

Custom Threshold Logic

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);
}

Integration with Analytics

'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>
  );
}

TypeScript

This package includes comprehensive TypeScript definitions:

import type { 
  RouteSuggestion, 
  SuggestedRoutesProps 
} from 'route-hints';

const suggestion: RouteSuggestion = {
  path: '/products',
  distance: 1
};

Contributing

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add some amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

License

MIT © [Your Name]

Support


Made with ❤️ for the Next.js community

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors