routing-patterns

📁 farming-labs/fm-skills 📅 7 days ago
1
总安装量
1
周安装量
#55134
全站排名
安装命令
npx skills add https://github.com/farming-labs/fm-skills --skill routing-patterns

Agent 安装分布

crush 1
amp 1
opencode 1
kimi-cli 1
codex 1
github-copilot 1

Skill 文档

Routing Patterns

Overview

Routing determines how URLs map to content and how navigation between pages works. The approach differs significantly between traditional server-rendered apps and modern SPAs.

Server-Side Routing

Server determines what to render based on the URL.

How It Works

1. User navigates to /about
2. Browser sends request to server
3. Server matches /about to handler
4. Server renders HTML
5. Server sends complete HTML response
6. Browser loads new page (full reload)

Characteristics

Request: GET /products/123

Server:
  Route table:
    /            → HomeController
    /about       → AboutController
    /products/:id → ProductController  ← matches

  ProductController:
    - Fetches product 123
    - Renders HTML
    - Returns response

Pros and Cons

Pros Cons
SEO-friendly (full HTML) Full page reload
Simple mental model Slower navigation
Works without JavaScript State lost between pages
Direct URL to content mapping Server must handle every route

Client-Side Routing

JavaScript handles routing in the browser, no server round-trip for navigation.

How It Works

1. Initial: Browser loads app shell + JS
2. User clicks link
3. JavaScript intercepts click (preventDefault)
4. JS updates URL using History API
5. JS renders new view/component
6. No server request for HTML

The History API

// Push new URL to history (no page reload)
history.pushState({ page: 'about' }, '', '/about');

// Replace current URL
history.replaceState({ page: 'home' }, '', '/');

// Listen for back/forward navigation
window.addEventListener('popstate', (event) => {
  // Render based on current URL
  renderRoute(window.location.pathname);
});

Basic Router Implementation

// Simplified client-side router concept
class Router {
  constructor() {
    this.routes = {};
    window.addEventListener('popstate', () => this.handleRoute());
  }

  register(path, component) {
    this.routes[path] = component;
  }

  navigate(path) {
    history.pushState({}, '', path);
    this.handleRoute();
  }

  handleRoute() {
    const path = window.location.pathname;
    const component = this.routes[path] || this.routes['/404'];
    component.render();
  }
}

// Usage
router.register('/about', AboutComponent);
router.register('/products/:id', ProductComponent);

Link Interception

// Intercept link clicks for client-side navigation
document.addEventListener('click', (e) => {
  const link = e.target.closest('a');
  if (link && link.href.startsWith(window.location.origin)) {
    e.preventDefault();
    router.navigate(link.pathname);
  }
});

File-Based Routing

Route structure derived from file system layout.

Common Patterns

File System                          URL
──────────────────────────────       ─────────────
pages/index.tsx                  →   /
pages/about.tsx                  →   /about
pages/blog/index.tsx             →   /blog
pages/blog/[slug].tsx            →   /blog/:slug
pages/docs/[...path].tsx         →   /docs/*
pages/[category]/[id].tsx        →   /:category/:id

Framework Implementations

Next.js (App Router):

app/
├── page.tsx                    → /
├── about/page.tsx              → /about
├── blog/[slug]/page.tsx        → /blog/:slug
└── (marketing)/                → Route group (no URL segment)
    └── pricing/page.tsx        → /pricing

SvelteKit:

src/routes/
├── +page.svelte                → /
├── about/+page.svelte          → /about
├── blog/[slug]/+page.svelte    → /blog/:slug
└── [[optional]]/+page.svelte   → / or /:optional

Remix:

app/routes/
├── _index.tsx                  → /
├── about.tsx                   → /about
├── blog._index.tsx             → /blog
├── blog.$slug.tsx              → /blog/:slug
└── $.tsx                       → Catch-all (splat)

Dynamic Routes

Parameter Types

Static:     /about          → Exact match
Dynamic:    /blog/[slug]    → Single parameter
Catch-all:  /docs/[...path] → Multiple segments
Optional:   /[[locale]]/    → With or without parameter

Parameter Access

Concept (framework-agnostic):

// URL: /products/shoes/nike-air-max

// Route: /products/[category]/[id]
// Parameters: { category: 'shoes', id: 'nike-air-max' }

// Route: /products/[...path]
// Parameters: { path: ['shoes', 'nike-air-max'] }

Route Groups and Layouts

Nested Layouts

app/
├── layout.tsx              → Root layout (applies to all)
├── (shop)/
│   ├── layout.tsx          → Shop layout (header, cart)
│   ├── products/page.tsx   → /products (uses shop layout)
│   └── cart/page.tsx       → /cart (uses shop layout)
└── (marketing)/
    ├── layout.tsx          → Marketing layout (different nav)
    ├── about/page.tsx      → /about (uses marketing layout)
    └── pricing/page.tsx    → /pricing (uses marketing layout)

Parallel Routes

Load multiple views simultaneously:

app/
├── @sidebar/
│   └── page.tsx            → Sidebar content
├── @main/
│   └── page.tsx            → Main content
└── layout.tsx              → Renders both in parallel

Intercepting Routes

Handle routes differently in different contexts:

app/
├── photos/[id]/page.tsx        → Full page view
├── @modal/(.)photos/[id]/       → Modal overlay (when navigating internally)
│   └── page.tsx
└── feed/page.tsx                → Grid with links to photos

Navigation Patterns

Declarative Navigation

// React Router / Next.js
<Link href="/about">About</Link>

// Vue Router
<router-link to="/about">About</router-link>

// SvelteKit
<a href="/about">About</a>  <!-- Enhanced automatically -->

Programmatic Navigation

// React Router
const navigate = useNavigate();
navigate('/dashboard');

// Next.js
const router = useRouter();
router.push('/dashboard');

// Vue Router
router.push('/dashboard');

// SvelteKit
import { goto } from '$app/navigation';
goto('/dashboard');

Navigation Options

// Replace history (no back button)
router.replace('/login');

// With query parameters
router.push('/search?q=term');

// With hash
router.push('/docs#section');

// Scroll to top
router.push('/page', { scroll: true });

// Shallow routing (no data refetch)
router.push('/page', { shallow: true });

Route Protection

Authentication Guards

// Middleware pattern (Next.js)
export function middleware(request) {
  const token = request.cookies.get('auth-token');
  
  if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
    return NextResponse.redirect(new URL('/login', request.url));
  }
}

// Route-level check
export async function loader({ request }) {
  const user = await getUser(request);
  if (!user) {
    throw redirect('/login');
  }
  return { user };
}

Authorization Patterns

// Role-based routing
if (!user.roles.includes('admin')) {
  redirect('/unauthorized');
}

// Feature flags
if (!features.newDashboard) {
  redirect('/legacy-dashboard');
}

URL State Management

Query Parameters

// Read query params
const searchParams = useSearchParams();
const query = searchParams.get('q');

// Update query params
const params = new URLSearchParams(searchParams);
params.set('page', '2');
router.push(`/products?${params.toString()}`);

Hash-Based State

// URL: /docs#installation
// Scroll to section
useEffect(() => {
  const hash = window.location.hash;
  if (hash) {
    document.querySelector(hash)?.scrollIntoView();
  }
}, []);

State in URL vs Component

URL State (shareable, bookmarkable):
  - Current page, filters, search query
  - Tab selection
  - Modal open state (sometimes)

Component State (ephemeral):
  - Form input before submission
  - Hover/focus states
  - Temporary UI state

Performance Patterns

Prefetching

// Next.js - automatic on hover
<Link href="/about" prefetch={true}>About</Link>

// Manual prefetch
router.prefetch('/dashboard');

Code Splitting by Route

// Lazy load route components
const Dashboard = lazy(() => import('./Dashboard'));

// Routes load only when accessed
<Route path="/dashboard" element={
  <Suspense fallback={<Loading />}>
    <Dashboard />
  </Suspense>
} />

Route Transitions

// Loading states during navigation
const navigation = useNavigation();

{navigation.state === 'loading' && <LoadingBar />}

Common Routing Pitfalls

1. Breaking the Back Button

// Bad: Replaces every navigation
router.replace('/page');  // Can't go back

// Good: Push for normal navigation
router.push('/page');  // Back button works

2. Not Handling 404s

// Ensure catch-all route exists
// app/[...not-found]/page.tsx or pages/404.tsx

3. Client-Only Routes in SSR

// Bad: useSearchParams in server component
// Causes hydration mismatch

// Good: Use in client component or read in middleware

4. Hash URLs for SEO Content

// Bad: /#/products/123 (not indexable)
// Good: /products/123 (indexable)

Deep Dive: Understanding Routing From First Principles

What is a URL? Anatomy of Web Addresses

Before understanding routing, you must understand URLs (Uniform Resource Locators):

https://www.example.com:443/products/shoes?color=red&size=10#reviews
│       │              │   │              │                    │
│       │              │   │              │                    └─ Fragment (client-only, not sent to server)
│       │              │   │              │
│       │              │   │              └─ Query String (key-value pairs)
│       │              │   │
│       │              │   └─ Pathname (the route)
│       │              │
│       │              └─ Port (usually implicit: 80 for http, 443 for https)
│       │
│       └─ Hostname (domain)
│
└─ Protocol (scheme)

Key insight: Only protocol, hostname, port, pathname, and query are sent to the server. The fragment (#hash) stays in the browser.

The History of Web Routing

Era 1: File-Based (1990s)

URL: /products/shoes.html
Server: Find file at /var/www/products/shoes.html, return it

URLs literally mapped to files on disk.

Era 2: Dynamic Routing (2000s)

URL: /products.php?id=123
Server: Execute products.php, which reads $_GET['id'] = 123

Server-side scripts processed parameters.

Era 3: RESTful Routes (2010s)

URL: /products/123
Server: Route pattern /products/:id matches, extract id=123

Clean URLs with pattern matching.

Era 4: Client-Side Routing (2010s-present)

URL: /products/123
Client: JavaScript intercepts, renders Product component with id=123
Server: May never see this request (after initial load)

How Server-Side Routing Actually Works

When a server receives a request, it must decide what to do:

// Express.js example - simplified internal logic
class Router {
  constructor() {
    this.routes = [];
  }
  
  // Register route
  get(pattern, handler) {
    this.routes.push({
      method: 'GET',
      pattern: this.parsePattern(pattern),  // /users/:id → regex
      handler
    });
  }
  
  // Pattern to regex conversion
  parsePattern(pattern) {
    // /users/:id → /^\/users\/([^\/]+)$/
    // /posts/:id/comments → /^\/posts\/([^\/]+)\/comments$/
    const regexStr = pattern
      .replace(/:([^/]+)/g, '([^/]+)')  // :param → capture group
      .replace(/\//g, '\\/');            // Escape slashes
    return new RegExp(`^${regexStr}$`);
  }
  
  // Handle incoming request
  handle(req, res) {
    const { method, url } = req;
    const pathname = new URL(url, 'http://localhost').pathname;
    
    // Find matching route
    for (const route of this.routes) {
      if (route.method !== method) continue;
      
      const match = pathname.match(route.pattern);
      if (match) {
        // Extract params
        req.params = this.extractParams(route.pattern, match);
        return route.handler(req, res);
      }
    }
    
    // No match - 404
    res.status(404).send('Not Found');
  }
}

Route matching order matters:

// These are checked in order
app.get('/users/new', newUserHandler);     // Must be BEFORE :id
app.get('/users/:id', getUserHandler);     // Would match "new" as id otherwise

// Request: GET /users/new
// Check 1: /users/new matches! → newUserHandler
// Never reaches /users/:id

// Request: GET /users/123
// Check 1: /users/new doesn't match
// Check 2: /users/:id matches! → getUserHandler with id=123

How Client-Side Routing Intercepts Browser Behavior

The browser has default navigation behavior. Client-side routing overrides it:

// DEFAULT BROWSER BEHAVIOR:
// 1. User clicks <a href="/about">
// 2. Browser sees href attribute
// 3. Browser sends GET request to /about
// 4. Browser receives HTML response
// 5. Browser replaces entire page

// SPA INTERCEPTION:
document.addEventListener('click', (event) => {
  // Find the link element (might be nested: <a><span>Click</span></a>)
  const anchor = event.target.closest('a');
  if (!anchor) return;  // Not a link click
  
  // Check if it's an internal link
  const url = new URL(anchor.href);
  if (url.origin !== window.location.origin) return;  // External link
  
  // Check for special cases
  if (anchor.hasAttribute('download')) return;  // Download link
  if (anchor.target === '_blank') return;       // New tab
  if (event.metaKey || event.ctrlKey) return;   // Cmd/Ctrl+click
  
  // NOW we intercept
  event.preventDefault();  // STOP browser's default behavior
  
  // Handle navigation ourselves
  navigateTo(url.pathname + url.search + url.hash);
});

function navigateTo(path) {
  // Update URL bar without page reload
  window.history.pushState({ path }, '', path);
  
  // Render the appropriate component
  renderRoute(path);
}

The History API in Detail

The History API is what makes SPAs possible without hash URLs:

// The history stack is like browser tabs' back/forward buttons
// But for a single tab:

// Initial state
// History: [ {url: '/'} ]
//                  ↑ current

// User navigates to /about
history.pushState({ page: 'about' }, '', '/about');
// History: [ {url: '/'}, {url: '/about'} ]
//                              ↑ current

// User navigates to /contact
history.pushState({ page: 'contact' }, '', '/contact');
// History: [ {url: '/'}, {url: '/about'}, {url: '/contact'} ]
//                                              ↑ current

// User clicks back button
// Browser fires 'popstate' event
// History: [ {url: '/'}, {url: '/about'}, {url: '/contact'} ]
//                              ↑ current (moved back)

// User clicks forward button
// Browser fires 'popstate' event again
// History: [ {url: '/'}, {url: '/about'}, {url: '/contact'} ]
//                                              ↑ current (moved forward)

// IMPORTANT: pushState does NOT fire popstate
// Only back/forward buttons fire popstate
// Your router must handle both!

Handling popstate:

window.addEventListener('popstate', (event) => {
  // event.state contains the state object from pushState
  // If user used back button, this fires with previous state
  
  // The URL has ALREADY changed when this fires
  const currentPath = window.location.pathname;
  
  renderRoute(currentPath);
});

Hash Routing vs History Routing

Before the History API (HTML5, ~2010), SPAs used hash-based routing:

// HASH ROUTING:
// URLs look like: /#/about, /#/products/123

// Why hashes? The hash fragment is NEVER sent to server
// So changing it doesn't trigger page reload

// Navigation:
window.location.hash = '#/about';

// Listening for changes:
window.addEventListener('hashchange', () => {
  const route = window.location.hash.slice(1);  // Remove #
  renderRoute(route);
});

// PROBLEMS WITH HASH ROUTING:
// 1. SEO: Crawlers ignore hash fragments
//    /products/shoes and /products/shoes#/details are same URL to Google
//
// 2. Ugly URLs: example.com/#/products/shoes vs example.com/products/shoes
//
// 3. Server can't respond to routes: everything goes to root

History routing advantages:

// HISTORY ROUTING:
// URLs look like: /about, /products/123 (clean!)

// Navigation:
history.pushState({}, '', '/about');

// Server sees the real URL
// SEO works properly
// Clean, professional URLs

// REQUIREMENT: Server must handle all routes
// Server must return the SPA for ANY route
// Otherwise: /products/123 → 404 if user refreshes

File-Based Routing: How It Works Internally

Meta-frameworks use file system as routing configuration:

// At build time, the framework scans your files:

// File structure:
// app/
// ├── page.tsx          → /
// ├── about/page.tsx    → /about
// └── blog/[slug]/page.tsx → /blog/:slug

// Framework generates route table:
const routes = [
  { pattern: /^\/$/, component: () => import('./app/page.tsx') },
  { pattern: /^\/about$/, component: () => import('./app/about/page.tsx') },
  { pattern: /^\/blog\/([^/]+)$/, component: () => import('./app/blog/[slug]/page.tsx') },
];

// Dynamic segments become route parameters:
// [slug] → :slug → captured as params.slug
// [id] → :id → captured as params.id
// [...path] → * → captured as params.path (array)

Why file-based routing became popular:

EXPLICIT ROUTING (React Router, Express):
- Route definitions separate from components
- Easy to have mismatched routes/files
- Must manually update both

// routes.tsx
<Route path="/products/:id" component={ProductPage} />

// ProductPage.tsx (might be anywhere)
export function ProductPage() { ... }


FILE-BASED ROUTING:
- Location IS the route
- Impossible to have orphan components
- Adding page = adding route automatically

// app/products/[id]/page.tsx (location defines route)
export default function ProductPage() { ... }

Route Parameters: Extraction and Typing

Understanding how parameters flow from URL to code:

// URL: /products/shoes/nike-air-max/reviews

// Route pattern: /products/[category]/[productId]/reviews
// Regex: /^\/products\/([^/]+)\/([^/]+)\/reviews$/

// Matching process:
const match = url.match(pattern);
// match = [
//   '/products/shoes/nike-air-max/reviews',  // Full match
//   'shoes',                                   // Group 1: category
//   'nike-air-max'                            // Group 2: productId
// ]

// Framework extracts to params object:
const params = {
  category: 'shoes',
  productId: 'nike-air-max'
};

// Your component receives these:
function ProductReviewsPage({ params }) {
  console.log(params.category);   // 'shoes'
  console.log(params.productId);  // 'nike-air-max'
}

Catch-all routes:

// Route: /docs/[...path]
// URL: /docs/getting-started/installation/windows

// The [...path] captures EVERYTHING after /docs/
const params = {
  path: ['getting-started', 'installation', 'windows']  // Array!
};

// Useful for:
// - Documentation with arbitrary depth
// - File browsers
// - Fallback/404 routes

Nested Routes and Layouts: The Component Tree

Nested routing creates component hierarchies:

URL: /dashboard/settings/profile

Route hierarchy:
app/
├── layout.tsx           ← Root layout (nav, footer)
└── dashboard/
    ├── layout.tsx       ← Dashboard layout (sidebar)
    └── settings/
        ├── layout.tsx   ← Settings layout (tabs)
        └── profile/
            └── page.tsx ← Profile content

Renders as:
<RootLayout>              {/* Always present */}
  <DashboardLayout>       {/* Present for /dashboard/* */}
    <SettingsLayout>      {/* Present for /dashboard/settings/* */}
      <ProfilePage />     {/* The actual content */}
    </SettingsLayout>
  </DashboardLayout>
</RootLayout>

Why nested layouts matter:

// WITHOUT nested layouts:
// Every page must include all wrappers

function DashboardSettingsProfilePage() {
  return (
    <RootLayout>
      <DashboardSidebar />
      <SettingsTabs />
      <ProfileForm />   {/* Actual unique content */}
    </RootLayout>
  );
}

function DashboardSettingsSecurityPage() {
  return (
    <RootLayout>           {/* Duplicated */}
      <DashboardSidebar /> {/* Duplicated */}
      <SettingsTabs />     {/* Duplicated */}
      <SecurityForm />     {/* Actual unique content */}
    </RootLayout>
  );
}

// WITH nested layouts:
// Layouts persist, only inner content changes

// Navigating from /dashboard/settings/profile
// to /dashboard/settings/security:
// - RootLayout: PRESERVED (no re-render)
// - DashboardLayout: PRESERVED (no re-render)
// - SettingsLayout: PRESERVED (no re-render)
// - Only ProfilePage → SecurityPage changes!

Route Guards and Middleware: The Request Pipeline

Routes often need protection or preprocessing:

// THE MIDDLEWARE CHAIN:
// Request flows through layers before reaching handler

// Request: GET /dashboard/admin

// Layer 1: Logging middleware
function loggingMiddleware(request, next) {
  console.log(`${request.method} ${request.url}`);
  return next(request);  // Continue to next middleware
}

// Layer 2: Authentication middleware
function authMiddleware(request, next) {
  const token = request.cookies.get('auth-token');
  if (!token) {
    return redirect('/login');  // Short-circuit: stop here
  }
  request.user = decodeToken(token);
  return next(request);  // Continue to next middleware
}

// Layer 3: Authorization middleware
function adminMiddleware(request, next) {
  if (!request.user.isAdmin) {
    return redirect('/unauthorized');
  }
  return next(request);
}

// Layer 4: Route handler
function adminDashboardHandler(request) {
  return renderPage(<AdminDashboard user={request.user} />);
}

// Request flows:
// loggingMiddleware → authMiddleware → adminMiddleware → adminDashboardHandler
//                           ↓
//                    (if no token)
//                           ↓
//                    redirect('/login')

Prefetching: How Routers Optimize Navigation

Modern routers predict user navigation:

// HOVER PREFETCHING:
// When user hovers over a link, likely to click

<Link href="/about" prefetch>About</Link>

// Framework behavior:
link.addEventListener('mouseenter', () => {
  // Start loading the /about route code and data
  router.prefetch('/about');
});

// VIEWPORT PREFETCHING:
// Prefetch links visible on screen

const observer = new IntersectionObserver((entries) => {
  entries.forEach((entry) => {
    if (entry.isIntersecting) {
      const href = entry.target.getAttribute('href');
      router.prefetch(href);
    }
  });
});

document.querySelectorAll('a[prefetch]').forEach((link) => {
  observer.observe(link);
});

// WHAT PREFETCH ACTUALLY DOES:
// 1. Loads the route's JavaScript chunk
// 2. May also preload data (framework-dependent)
// 3. Stores in memory for instant navigation

Scroll Restoration: The Hidden Complexity

When navigating, browsers must manage scroll position:

// BROWSER DEFAULT BEHAVIOR:
// - New navigation: scroll to top
// - Back/forward: restore previous scroll position

// SPA CHALLENGE:
// Browser doesn't know we "navigated" - URL changed via JavaScript
// Must manually handle scroll:

function navigateTo(path) {
  // Save current scroll position
  const scrollPosition = window.scrollY;
  history.replaceState(
    { ...history.state, scrollY: scrollPosition },
    ''
  );
  
  // Navigate
  history.pushState({ path, scrollY: 0 }, '', path);
  
  // Scroll to top for new navigation
  window.scrollTo(0, 0);
}

window.addEventListener('popstate', (event) => {
  renderRoute(window.location.pathname);
  
  // Restore scroll position for back/forward
  if (event.state?.scrollY !== undefined) {
    // Wait for content to render
    requestAnimationFrame(() => {
      window.scrollTo(0, event.state.scrollY);
    });
  }
});

The Edge Cases Every Router Must Handle

// 1. TRAILING SLASHES
// /about vs /about/ - are they the same?
// Your router must decide and be consistent
// Usually: redirect one to the other

// 2. CASE SENSITIVITY
// /About vs /about - same route?
// URLs are technically case-sensitive
// Best practice: lowercase, redirect others

// 3. ENCODED CHARACTERS
// /products/running%20shoes = /products/running shoes
// Router must decode: decodeURIComponent(path)

// 4. DOUBLE SLASHES
// /products//shoes - valid?
// Should probably normalize: /products/shoes

// 5. DOT SEGMENTS
// /products/../about = /about
// May need to resolve relative paths

// 6. QUERY STRING HANDLING
// /search?q=foo vs /search?q=bar
// Same route, different parameters
// Router matches path, your code handles query

// 7. HASH FRAGMENTS
// /products#reviews
// Hash not sent to server
// Client must scroll to #reviews element

For Framework Authors: Building Routing Systems

Implementation Note: The patterns and code examples below represent one proven approach to building routing systems. Different frameworks take different approaches—React Router uses a declarative model, Next.js uses file-based routing, and SvelteKit combines both. The direction shown here provides core concepts that most routers share. Adapt these patterns based on your framework’s component model, build pipeline, and developer experience goals.

Implementing a Route Matcher

// ROUTE MATCHING IMPLEMENTATION

class RouteMatcher {
  constructor() {
    this.routes = [];
  }
  
  // Add route with pattern
  add(pattern, handler, meta = {}) {
    const { regex, paramNames, score } = this.compilePattern(pattern);
    this.routes.push({ pattern, regex, paramNames, handler, meta, score });
    // Keep sorted by specificity
    this.routes.sort((a, b) => b.score - a.score);
  }
  
  // Compile pattern to regex
  compilePattern(pattern) {
    const paramNames = [];
    let score = 0;
    
    const regexStr = pattern
      .split('/')
      .filter(Boolean)
      .map(segment => {
        // Catch-all: [...param]
        if (segment.startsWith('[...') && segment.endsWith(']')) {
          paramNames.push(segment.slice(4, -1));
          score += 1; // Lowest priority
          return '(.+)';
        }
        
        // Optional: [[param]]
        if (segment.startsWith('[[') && segment.endsWith(']]')) {
          paramNames.push(segment.slice(2, -2));
          score += 5;
          return '([^/]*)';
        }
        
        // Dynamic: [param]
        if (segment.startsWith('[') && segment.endsWith(']')) {
          paramNames.push(segment.slice(1, -1));
          score += 10;
          return '([^/]+)';
        }
        
        // Static segment
        score += 100;
        return escapeRegex(segment);
      })
      .join('/');
    
    return {
      regex: new RegExp(`^/${regexStr}/?$`),
      paramNames,
      score,
    };
  }
  
  // Match URL to route
  match(pathname) {
    for (const route of this.routes) {
      const match = pathname.match(route.regex);
      if (match) {
        const params = {};
        route.paramNames.forEach((name, i) => {
          const value = match[i + 1];
          // Handle catch-all as array
          if (route.pattern.includes(`[...${name}]`)) {
            params[name] = value ? value.split('/') : [];
          } else {
            params[name] = value;
          }
        });
        
        return { route, params };
      }
    }
    return null;
  }
}

function escapeRegex(str) {
  return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

Building a File-Based Router Generator

// FILE-BASED ROUTING GENERATOR (Build Tool)

import { glob } from 'glob';
import path from 'path';
import { parse } from '@babel/parser';

async function generateRouteManifest(config) {
  const { pagesDir, extensions = ['.tsx', '.jsx', '.ts', '.js'] } = config;
  
  // Find all route files
  const pattern = `**/*{${extensions.join(',')}}`;
  const files = await glob(pattern, { cwd: pagesDir });
  
  const routes = [];
  
  for (const file of files) {
    // Skip special files
    if (file.startsWith('_') || file.includes('/_')) continue;
    
    const route = await processRouteFile(pagesDir, file);
    if (route) routes.push(route);
  }
  
  // Sort by specificity
  routes.sort((a, b) => b.score - a.score);
  
  return routes;
}

async function processRouteFile(pagesDir, file) {
  const fullPath = path.join(pagesDir, file);
  const content = await fs.readFile(fullPath, 'utf-8');
  const ast = parse(content, { sourceType: 'module', plugins: ['jsx', 'typescript'] });
  
  // Extract route metadata from exports
  const meta = extractRouteMeta(ast);
  
  // Convert file path to route pattern
  let route = file
    .replace(/\.(tsx?|jsx?)$/, '')    // Remove extension
    .replace(/\/index$/, '')           // /index -> /
    .replace(/\[\.\.\.(\w+)\]/g, '*')  // [...slug] -> *
    .replace(/\[\[(\w+)\]\]/g, ':$1?') // [[id]] -> :id?
    .replace(/\[(\w+)\]/g, ':$1');     // [id] -> :id
  
  if (!route) route = '/';
  else if (!route.startsWith('/')) route = '/' + route;
  
  return {
    path: route,
    file: fullPath,
    ...meta,
    score: calculateScore(route),
  };
}

function extractRouteMeta(ast) {
  const meta = {};
  
  // Look for exported config
  for (const node of ast.program.body) {
    if (node.type === 'ExportNamedDeclaration') {
      if (node.declaration?.declarations?.[0]?.id?.name === 'config') {
        // Extract static config
        meta.config = evaluateStaticObject(node.declaration.declarations[0].init);
      }
    }
  }
  
  return meta;
}

// Generate route manifest code
function emitRouteManifest(routes) {
  return `
// Auto-generated route manifest
export const routes = [
${routes.map(r => `  {
    path: ${JSON.stringify(r.path)},
    component: () => import(${JSON.stringify(r.file)}),
    meta: ${JSON.stringify(r.meta || {})},
  }`).join(',\n')}
];
`;
}

Implementing Nested Routes and Layouts

// NESTED ROUTING IMPLEMENTATION

class NestedRouter {
  constructor() {
    this.root = { children: [], layout: null, page: null };
  }
  
  // Build route tree from flat routes
  buildTree(routes) {
    for (const route of routes) {
      this.insertRoute(route);
    }
  }
  
  insertRoute(route) {
    const segments = route.path.split('/').filter(Boolean);
    let node = this.root;
    
    for (let i = 0; i < segments.length; i++) {
      const segment = segments[i];
      let child = node.children.find(c => c.segment === segment);
      
      if (!child) {
        child = { segment, children: [], layout: null, page: null };
        node.children.push(child);
      }
      
      node = child;
    }
    
    // Assign component based on file type
    if (route.file.includes('layout')) {
      node.layout = route.component;
    } else if (route.file.includes('page')) {
      node.page = route.component;
    }
  }
  
  // Match and collect all layouts + page
  match(pathname) {
    const segments = pathname.split('/').filter(Boolean);
    const matched = [];
    let node = this.root;
    const params = {};
    
    // Always include root layout
    if (node.layout) matched.push({ component: node.layout, params: {} });
    
    for (const segment of segments) {
      // Find matching child
      let child = node.children.find(c => c.segment === segment);
      
      // Try dynamic match
      if (!child) {
        child = node.children.find(c => c.segment.startsWith(':'));
        if (child) {
          const paramName = child.segment.slice(1);
          params[paramName] = segment;
        }
      }
      
      if (!child) return null; // No match
      
      if (child.layout) {
        matched.push({ component: child.layout, params: { ...params } });
      }
      
      node = child;
    }
    
    // Add final page
    if (node.page) {
      matched.push({ component: node.page, params: { ...params }, isPage: true });
    }
    
    return { matched, params };
  }
}

// Render nested routes with outlet pattern
async function renderNestedRoute(matched) {
  let content = null;
  
  // Render inside-out (page first, then wrap with layouts)
  for (let i = matched.length - 1; i >= 0; i--) {
    const { component, params, isPage } = matched[i];
    const Component = await component();
    
    if (isPage) {
      content = createElement(Component.default, { params });
    } else {
      // Layout receives children as outlet
      content = createElement(Component.default, { 
        params, 
        children: content,
      });
    }
  }
  
  return content;
}

Route Preloading System

// ROUTE PRELOADING IMPLEMENTATION

class RoutePreloader {
  constructor(router) {
    this.router = router;
    this.preloaded = new Set();
    this.preloading = new Map();
  }
  
  // Preload on hover
  setupHoverPreload() {
    document.addEventListener('mouseenter', (e) => {
      const link = e.target.closest('a[href]');
      if (!link) return;
      
      const href = link.getAttribute('href');
      if (href.startsWith('/') && !this.preloaded.has(href)) {
        this.preload(href);
      }
    }, true);
  }
  
  // Preload visible links
  setupViewportPreload() {
    const observer = new IntersectionObserver((entries) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          const href = entry.target.getAttribute('href');
          if (!this.preloaded.has(href)) {
            // Use requestIdleCallback for low-priority preload
            requestIdleCallback(() => this.preload(href));
          }
        }
      });
    }, { rootMargin: '200px' });
    
    document.querySelectorAll('a[href^="/"]').forEach(link => {
      observer.observe(link);
    });
  }
  
  async preload(pathname) {
    if (this.preloading.has(pathname)) {
      return this.preloading.get(pathname);
    }
    
    const match = this.router.match(pathname);
    if (!match) return;
    
    const preloadPromise = (async () => {
      // Preload component code
      const componentPromises = match.matched.map(m => m.component());
      
      // Preload data if route has loader
      const route = match.matched.find(m => m.isPage);
      const dataPromise = route?.loader?.({ 
        params: match.params, 
        preload: true 
      });
      
      await Promise.all([...componentPromises, dataPromise]);
      this.preloaded.add(pathname);
    })();
    
    this.preloading.set(pathname, preloadPromise);
    
    try {
      await preloadPromise;
    } finally {
      this.preloading.delete(pathname);
    }
  }
}

Navigation State Management

// NAVIGATION STATE IMPLEMENTATION

class NavigationManager {
  constructor() {
    this.state = 'idle'; // idle | loading | submitting
    this.location = window.location.pathname;
    this.listeners = new Set();
  }
  
  subscribe(listener) {
    this.listeners.add(listener);
    return () => this.listeners.delete(listener);
  }
  
  notify() {
    this.listeners.forEach(l => l({
      state: this.state,
      location: this.location,
    }));
  }
  
  async navigate(to, options = {}) {
    const { replace = false, state = {} } = options;
    
    this.state = 'loading';
    this.notify();
    
    try {
      // Run before navigation hooks
      for (const hook of this.beforeHooks) {
        const result = await hook(this.location, to);
        if (result === false) {
          this.state = 'idle';
          this.notify();
          return;
        }
        if (typeof result === 'string') {
          to = result; // Redirect
        }
      }
      
      // Update URL
      if (replace) {
        history.replaceState(state, '', to);
      } else {
        history.pushState(state, '', to);
      }
      
      this.location = to;
      
      // Load and render route
      await this.renderRoute(to);
      
      // Scroll to top or hash
      if (to.includes('#')) {
        document.querySelector(to.split('#')[1])?.scrollIntoView();
      } else {
        window.scrollTo(0, 0);
      }
      
    } catch (error) {
      console.error('Navigation failed:', error);
      throw error;
    } finally {
      this.state = 'idle';
      this.notify();
    }
  }
}

// React hook for navigation state
function useNavigation() {
  const [state, setState] = useState({ state: 'idle', location: '' });
  
  useEffect(() => {
    return navigationManager.subscribe(setState);
  }, []);
  
  return state;
}

// Usage in components
function LoadingBar() {
  const { state } = useNavigation();
  
  return state === 'loading' ? <ProgressBar /> : null;
}

Related Skills