Building the Navigator: A Route Search Tool

By Claude Sonnet 4.5  ·  January 17, 2026
docsProject: jaygriffFeature: Navigator
🏷️ Tags:searchroutingarchitecturereactnextjsdev-docsfeatures

A complete walkthrough of the Navigator feature - a popover search tool for quickly finding and navigating to any route in the application.

The Navigator is a popover search tool that lets you quickly find and jump to any route in the application. Think of it as a command palette for navigation - type to search, click to navigate.

What It Does

The Navigator provides instant search across all routes in your site:

  • Click the arrow icon in the navbar to open the Navigator popover
  • Type to search across route titles, paths, descriptions, and keywords
  • Click any result to navigate to that route
  • Press Escape or click outside to close the popover

It automatically indexes all routes from your sitemap and enriches them with metadata from posts (titles, descriptions, topics, and tags become searchable keywords).

File Structure

The Navigator feature consists of 4 files:

  • src/lib/routes.ts - Server-side route indexing logic
  • src/components/Navigator.tsx - Client-side search UI component
  • src/components/NavBar.tsx - Integration point (trigger button + state management)
  • src/app/layout.tsx - Data fetching (calls getAllRoutes() and passes to NavBar)

Architecture Decisions

Why Sitemap as Source of Truth?

We use the sitemap as the single source of truth for routes because:

  • Sitemap already knows about all public routes (it's required for SEO)
  • No duplication - we don't maintain a separate route registry
  • Auto-scaling - new pages automatically show up in Navigator when they're added to sitemap
  • Zero maintenance - as routes change, sitemap updates, Navigator reflects changes

Why Popover Instead of Modal?

The Navigator uses a popover pattern (anchored to the trigger button) rather than a centered modal:

  • Feels more like a tool than an interruption
  • Contextual - it's clearly associated with the navbar button
  • Lighter weight - doesn't require a full-screen overlay
  • Faster to dismiss - clicking anywhere outside closes it

Server vs Client Components

We split data fetching and UI rendering between server and client:

  • Server (layout.tsx) - Fetches routes once at build time or request time
  • Client (Navigator.tsx) - Handles search input, filtering, and user interactions
  • This avoids client-side data fetching delays - routes are available immediately when popover opens

Code Walkthrough

1. Route Indexing (src/lib/routes.ts)

This file builds the searchable route index:

.ts
export interface RouteEntry {
  path: string;
  title: string;
  description?: string;
  keywords?: string[];
}

export async function getAllRoutes(): Promise<RouteEntry[]> {
  // Get all routes from sitemap
  const sitemapEntries = await sitemap();
  const posts = await getAllPosts();
  
  const routes: RouteEntry[] = sitemapEntries.map((entry) => {
    const url = new URL(entry.url);
    const path = url.pathname === '/' ? '/' : url.pathname;
    
    // Enrich post routes with metadata
    if (path.startsWith('/posts/')) {
      const slug = path.replace('/posts/', '');
      const post = posts.find(p => p.metadata.slug === slug);
      
      if (post) {
        return {
          path,
          title: post.metadata.title,
          description: post.metadata.description,
          keywords: [
            ...(post.metadata.tags || []),
            post.metadata.slug,
          ],
        };
      }
    }
    
    // Handle other routes...
  });
  
  return routes;
}

Key concepts:

  • Fetches sitemap entries to get all public URLs
  • Extracts path from each URL
  • For post routes, finds matching post and enriches with title, description, and keywords
  • Keywords include topics, tags, and slug - all searchable
  • Returns array of RouteEntry objects with structured data

2. Search UI (src/components/Navigator.tsx)

This component renders the popover and handles search:

.tsx
'use client';

export default function Navigator({ routes, onClose }: NavigatorProps) {
  const [searchQuery, setSearchQuery] = useState('');

  const filteredRoutes = useMemo(() => {
    if (!searchQuery.trim()) return routes;

    const query = searchQuery.toLowerCase();
    return routes.filter((route) => {
      const titleMatch = route.title.toLowerCase().includes(query);
      const pathMatch = route.path.toLowerCase().includes(query);
      const descriptionMatch = route.description?.toLowerCase().includes(query);
      const keywordMatch = route.keywords?.some((keyword) =>
        keyword.toLowerCase().includes(query)
      );

      return titleMatch || pathMatch || descriptionMatch || keywordMatch;
    });
  }, [routes, searchQuery]);

  return (
    <div css={navigatorStyles}>
      <input
        type="text"
        placeholder="Search routes..."
        value={searchQuery}
        onChange={(e) => setSearchQuery(e.target.value)}
        autoFocus
      />
      <div css={resultsContainerStyles}>
        {filteredRoutes.map((route) => (
          <Link key={route.path} href={route.path} onClick={onClose}>
            <div>{route.title}</div>
            <div>{route.path}</div>
            {route.description && <div>{route.description}</div>}
          </Link>
        ))}
      </div>
    </div>
  );
}

Key concepts:

  • 'use client' - This is a Client Component (needs interactivity)
  • useState - Tracks search input value
  • useMemo - Optimizes filtering (only re-runs when routes or query changes)
  • Filter logic - Checks if query matches title, path, description, or any keyword
  • autoFocus - Input is focused when popover opens
  • onClose callback - Called when user clicks a route (closes popover)

3. Integration (src/components/NavBar.tsx)

The NavBar manages Navigator state and trigger button:

.tsx
export default function NavBar({ routes }: NavBarProps) {
  const [isNavigatorOpen, setIsNavigatorOpen] = useState(false);
  const navigatorRef = useRef<HTMLDivElement>(null);

  // Close on click outside or Escape key
  useEffect(() => {
    if (!isNavigatorOpen) return;

    const handleClickOutside = (event: MouseEvent) => {
      if (navigatorRef.current && !navigatorRef.current.contains(event.target as Node)) {
        setIsNavigatorOpen(false);
      }
    };

    const handleEscape = (event: KeyboardEvent) => {
      if (event.key === 'Escape') {
        setIsNavigatorOpen(false);
      }
    };

    document.addEventListener('mousedown', handleClickOutside);
    document.addEventListener('keydown', handleEscape);

    return () => {
      document.removeEventListener('mousedown', handleClickOutside);
      document.removeEventListener('keydown', handleEscape);
    };
  }, [isNavigatorOpen]);

  return (
    <nav>
      {/* Title and particles... */}
      
      <div ref={navigatorRef}>
        <button onClick={() => setIsNavigatorOpen(!isNavigatorOpen)}>
          <svg>{/* Arrow icon */}</svg>
        </button>
        {isNavigatorOpen && (
          <Navigator routes={routes} onClose={() => setIsNavigatorOpen(false)} />
        )}
      </div>
    </nav>
  );
}

Key concepts:

  • navigatorRef - Used to detect clicks outside the Navigator
  • useEffect cleanup - Event listeners are removed when popover closes
  • Click outside detection - Checks if click target is inside navigatorRef
  • Escape key handling - Standard pattern for dismissible overlays
  • Conditional render - Navigator only mounts when isNavigatorOpen is true

4. Data Fetching (src/app/layout.tsx)

The root layout fetches routes and passes them down:

.tsx
import { getAllRoutes } from '@/lib/routes';

export default async function RootLayout({ children }: { children: React.ReactNode }) {
  const routes = await getAllRoutes();
  
  return (
    <html>
      <body>
        <EmotionProvider>
          <NavBar routes={routes} />
          {children}
        </EmotionProvider>
      </body>
    </html>
  );
}

Key concepts:

  • async function - Layout is now a Server Component that can fetch data
  • await getAllRoutes() - Fetches routes at build time or request time
  • Props drilling - Routes passed to NavBar which passes to Navigator
  • This ensures routes are available immediately (no loading state needed in Navigator)

Styling Approach

All styles use Emotion CSS objects (not template literals):

.ts
const navigatorStyles = css({
  position: 'absolute',
  top: 'calc(100% + 8px)',
  right: 0,
  width: '400px',
  maxHeight: '500px',
  backgroundColor: 'rgba(20, 20, 30, 0.95)',
  backdropFilter: 'blur(10px)',
  border: '1px solid rgba(255, 255, 255, 0.1)',
  borderRadius: '12px',
  // ...
});

Why CSS objects?

  • Type-safe - TypeScript validates CSS properties
  • Composable - Objects can be merged programmatically
  • AI-friendly - Structured data easier to generate/modify than strings
  • No string escaping - Values are JavaScript, not template strings

Future Enhancements

Potential improvements for the Navigator:

  • Fuzzy search - Use a library like Fuse.js for better matching (typo tolerance)
  • Keyboard navigation - Arrow keys to select results, Enter to navigate
  • Recent/frequent routes - Track usage and show popular routes first
  • Keyboard shortcut - Cmd+K to open Navigator from anywhere
  • Search highlighting - Highlight matching text in results
  • Route categories - Group results by type (posts, pages, docs)

Testing the Navigator

To test the Navigator feature:

  • Click the arrow icon in the navbar (top-right corner)
  • Popover should open with search input focused
  • All routes should be visible initially
  • Type a query - results should filter in real-time
  • Search works across titles, paths, descriptions, and keywords
  • Click a result - should navigate to that route and close popover
  • Press Escape - should close popover
  • Click outside - should close popover

Common Patterns

This feature demonstrates several important React patterns:

  • Server/Client split - Data fetching server-side, interaction client-side
  • useMemo optimization - Prevent expensive filtering on every render
  • useRef for DOM access - Detect clicks outside an element
  • useEffect cleanup - Remove event listeners to prevent memory leaks
  • Conditional rendering - Only mount components when needed
  • Callback props - Child component (Navigator) triggers parent state change (close popover)
  • Type safety - RouteEntry interface ensures data structure consistency

Summary

The Navigator is a practical feature that shows how to build a search tool with:

  • Automatic route indexing from sitemap (zero maintenance)
  • Metadata enrichment from posts (searchable descriptions and keywords)
  • Real-time filtering with React hooks (useMemo for performance)
  • Proper UX patterns (click outside, Escape to close, autofocus)
  • Server/Client separation (data fetching vs interactivity)
  • Type-safe structured data (TypeScript + Emotion CSS objects)

The code is clean, maintainable, and follows Next.js best practices. It scales automatically as new routes are added to the site.