| name | route-discovery-system |
| description | Auto-discovers Next.js App Router pages from the filesystem and renders navigable index pages with switchable display variants. Use when creating index/listing pages, adding new route directories, converting hard-coded route configs to auto-discovery, or adding new display variants for route listings. |
Route Discovery System
Filesystem-based route auto-discovery that replaces hard-coded config.ts route arrays. Server Components scan directories for page.tsx files and render grouped navigation UIs with switchable display variants.
Architecture
Server Component (RouteIndexPage)
โโ scanRoutes() reads filesystem (server-only)
โโ Serializes RouteDisplayData
โโ Passes to RouteDisplaySwitcher (client)
โโ next/dynamic loads active variant (ssr: false)
โโ Dropdown switches between variants
Key boundary: utils/route-discovery/index.ts is server-only (filesystem access). utils/route-discovery/shared.ts has pure functions safe for client imports.
File Map
| File | Role |
|---|
utils/route-discovery/index.ts | Server-only: scanRoutes, scanRoutesShallow |
utils/route-discovery/shared.ts | Client-safe: groupRoutes, getRouteLabel, toModulePages, sortGroupKeys |
components/ssr/RouteIndexPage.tsx | Server Component: scans filesystem, renders page shell, passes data to switcher |
components/ssr/RouteHeaderData.tsx | Server Component: auto-feeds ModuleHeader nav from filesystem |
components/ssr/route-display/types.ts | RouteDisplayData, RouteDisplayVariant, RouteDisplayProps |
components/ssr/route-display/RouteDisplaySwitcher.tsx | Client wrapper: variant dropdown + next/dynamic imports |
components/ssr/route-display/GroupedCardsDisplay.tsx | Variant: grouped folder cards (default) |
components/ssr/route-display/DataTableDisplay.tsx | Variant: searchable keyboard-navigable grid |
components/ssr/route-display/ExpandableSectionsDisplay.tsx | Variant: collapsible accordion sections |
components/ssr/route-display/FlatListDisplay.tsx | Variant: minimal filterable list |
Usage: Index Page
Replace any hard-coded route listing with:
import { join } from "path";
import { RouteIndexPage } from "@/components/ssr/RouteIndexPage";
export default async function MyPage() {
return (
<RouteIndexPage
directory={join(process.cwd(), "app", "(authenticated)", "my-feature")}
basePath="/my-feature"
title="My Feature"
/>
);
}
Props
| Prop | Type | Default | Description |
|---|
directory | string | required | Absolute path to scan |
basePath | string | required | URL prefix for links |
title | string | โ | Page heading |
description | string | auto-generated | Subtitle text |
icon | ComponentType | LayoutGrid | Lucide icon for heading |
shallow | boolean | false | Scan one level only |
defaultVariant | RouteDisplayVariant | "grouped-cards" | Initial display variant |
children | ReactNode | โ | Custom content above the grid |
Usage: Layout with Auto-Header
Replace config.ts + ModuleHeader patterns:
import { join } from "path";
import { RouteHeaderData } from "@/components/ssr/RouteHeaderData";
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<RouteHeaderData
directory={join(process.cwd(), "app", "(authenticated)", "tests", "forms")}
moduleHome="/tests/forms"
moduleName="Forms"
>
{children}
</RouteHeaderData>
);
}
Adding a New Display Variant
- Create
components/ssr/route-display/MyNewDisplay.tsx:
"use client";
import type { RouteDisplayProps } from "./types";
export default function MyNewDisplay({ data }: RouteDisplayProps) {
const { routes, groups, sortedGroupKeys, basePath, hasGroups } = data;
}
- Add the variant key to
types.ts:
export type RouteDisplayVariant =
| "grouped-cards"
| "data-table"
| "expandable-sections"
| "flat-list"
| "my-new-variant";
-
Add the label to VARIANT_LABELS in types.ts.
-
Register in RouteDisplaySwitcher.tsx:
"my-new-variant": dynamic(() => import("./MyNewDisplay"), { ssr: false }),
Scanner Behavior
- Recursively walks directories looking for
page.tsx files
- Skips:
_prefixed directories, [dynamic] route segments
- Groups routes by top-level directory segment
scanRoutesShallow only checks immediate children (no recursion)
Converting Existing Routes
When converting a directory from hard-coded config.ts to auto-discovery:
- Replace
page.tsx with RouteIndexPage usage
- Replace
layout.tsx with RouteHeaderData (if it imported from config.ts)
- Delete
config.ts
- Keep
EntityPack wrappers and export const dynamic = 'force-dynamic' if present
- Never touch functional leaf pages โ only replace listing/index pages
Favicon Integration
RouteIndexPage automatically resolves the favicon for the basePath and threads it into the display:
- The favicon badge renders next to the page
<h1> title (replaces the Lucide icon when a favicon exists)
GroupedCardsDisplay uses the favicon color as a left-border accent on each group card
- Flat list entries show the favicon badge instead of a plain dot
No extra code needed โ just pass the correct basePath and the system picks up the config from navigation-links.tsx or the system-route overrides (/demo, /tests, /administration).
To understand how favicons are registered and what colors/letters are already in use, see .claude/skills/route-metadata-favicons/SKILL.md.
Pages NOT Using This System
These have custom logic and intentionally remain hand-maintained:
administration/page.tsx โ curated category dashboard (has /administration/all-routes companion)
dashboard/page.tsx โ iOS-style icon grid
tests/links/ and tests/windows/ โ master cross-directory index lists
demos/local-tools/page.tsx, demos/scraper/page.tsx โ custom functional UIs