App Routes Path Strategy
Analysis and decision on how to handle path metadata for app routes: derive from file structure vs explicit metadata.
The Problem
App routes live in src/app/ with file-based routing. We want to list them alongside content in navigation, search, and the homepage. The question: should we store the URL path in metadata, or derive it?
Options Considered
Option 1: Always Derive Path
Calculate path from file structure in getAllAppRoutes():
// app/metadata-scanner/page.tsx → /metadata-scanner
// app/dev/test-parser/page.tsx → /dev/test-parserPros:
- No redundancy - file structure is source of truth
- Can't get out of sync
- Less metadata to maintain
Cons:
- No flexibility - path locked to file location
- Can't reorganize URLs without moving files
Option 2: Always Specify Path in Metadata
Require path in every routeMetadata export:
export const routeMetadata = {
title: 'Metadata Scanner',
slug: 'metadata-scanner',
path: '/metadata-scanner', // Required
// ...
};Pros:
- Explicit - always know the URL
- Full flexibility to route anywhere
Cons:
- Redundant - duplicates Next.js routing info
- Can drift from actual route (file says /dev, metadata says /tools)
- More to maintain for every route
Option 3: Optional Path Override (Recommended)
Derive path by default, allow metadata to override:
// Default: path derived from file location
export const routeMetadata = {
title: 'Metadata Scanner',
slug: 'metadata-scanner',
// No path needed - will be /metadata-scanner
};
// Override when needed
export const routeMetadata = {
title: 'Parser Test',
slug: 'test-parser',
path: '/dev/parser', // Override: file is at app/dev/test-parser but want /dev/parser
};Pros:
- Best of both worlds - minimal by default, flexible when needed
- No redundancy in common case (95%+ of routes)
- Escape hatch for edge cases (URL structure differs from file structure)
Cons:
- Slightly more complex logic (check metadata first, then derive)
Decision: Option 3
Use optional path override. This minimizes redundancy while preserving flexibility for the rare case where URL structure should differ from file structure.
Implementation
In getAllAppRoutes()
export async function getAllAppRoutes() {
// ...scan files...
for (const pagePath of pageFiles) {
const derivedPath = '/' + pagePath.replace('/page.tsx', '').replace('page.tsx', '');
const module = await import(`@/app/${pagePath.replace('.tsx', '')}`);
if (module.routeMetadata) {
routes.push({
filename: pagePath,
metadata: {
...module.routeMetadata,
// Use metadata path if provided, otherwise use derived path
path: module.routeMetadata.path || derivedPath,
},
});
}
}
}In PostMeta Interface
export interface PostMeta {
// ... other fields ...
path?: string; // Optional - overrides derived path for app routes
}In Navigation/Routing Logic
// HomePage, Navigator, etc.
const href = item.metadata.path || (
// Fallback for content files without path
item.metadata.type === 'doc:commit'
? `/docs/commits/${item.metadata.slug}`
: item.metadata.type === 'doc'
? `/docs/${item.metadata.slug}`
: `/posts/${item.metadata.slug}`
);Examples
Standard Case (No Override)
// File: app/metadata-scanner/page.tsx
export const routeMetadata = {
title: 'Metadata Scanner',
slug: 'metadata-scanner',
description: '...',
// NO path specified
};
// Result: path auto-derived as "/metadata-scanner"Override Case
// File: app/experimental/new-feature/page.tsx
export const routeMetadata = {
title: 'New Feature',
slug: 'new-feature',
path: '/features/new', // Override: want /features/new instead of /experimental/new-feature
};
// Result: path is "/features/new"When to Use Path Override
- URL restructuring: Want to change public URLs without moving files
- Vanity URLs: File is deeply nested but want short URL
- Migration: Moving to new structure but keeping old URLs
- Organizational mismatch: File organization differs from user-facing structure
Conclusion
The optional path override strategy balances DRY principles with practical flexibility. Most routes need no path metadata. The few that do can override cleanly without polluting every route's metadata.