Lightview Router

A lightweight, pipeline-based History API router with middleware support.

Overview

The Lightview Router provides a simple yet powerful way to handle client-side routing in your application. It supports standard routes, wildcards, named parameters, and middleware pipelines.

Features

Basic Usage

Initialize the router with a target element and define your routes.

import { LightviewRouter } from '/lightview-router.js';

// 1. Initialize
const appRouter = LightviewRouter.router({
    contentEl: document.getElementById('app'), // Routes will render here automatically
    
    // Optional: Lifecycle hooks
    // onStart: (path) => console.log('Loading...'),
    // notFound: (path) => console.log('404 Not Found')
});

// 2. Register routes
// Simple path mapping (fetches /index.html -> renders to #app)
appRouter.use('/', '/index.html');

// Self-mapping (fetches /about.html -> renders to #app)
appRouter.use('/about.html');

// Wildcards (fetches matching path -> renders to #app)
appRouter.use('/docs/*');

// 3. Start listening
appRouter.start();

Middleware & Pipelines

Lightview Router uses a "chain of responsibility" pattern. When you register a route, you can provide multiple handlers.

Automatic Rendering: If you provide contentEl and your route chain ends with a string (or has no handlers), the router automatically appends a handler that fetches the path and renders it to contentEl.

You can still define manual handlers for advanced logic:

➡️ Continue Chain

If a handler returns null, undefined, or a context object, the router continues to the next handler in the chain.

Use this for logging, auth checks, or transforming the path.

// Middleware
const logger = (ctx) => {
    console.log('Visiting:', ctx.path);
    // Returns undefined, so routing continues
};

🛑 Stop & Return

If a handler returns a Response object, the chain stops immediately, and the router considers the navigation complete.

Use this for custom API responses or redirects.

// Final Handler
const loadPage = async (ctx) => {
    return await fetch(ctx.path);
    // Returns Response, stops routing
};

Example Pipeline

appRouter.use('/admin/*', 
    // 1. Authentication Middleware
    (ctx) => {
        if (!user.isLoggedIn) {
            router.navigate('/login'); 
            return new Response('Redirecting...'); // Stop chain
        }
        // User is logged in, continue...
    },
    
    // 2. Logging Middleware
    (ctx) => {
        console.log('Admin access at', new Date());
        // implicitly returns undefined, continues...
    },

    // 3. Implicit Fetch (if contentEl is set)
    // The router automatically fetches /admin/* and renders it
);

Advanced Usage

Functional Arguments

The use(...) method accepts a variadic list of arguments. While standard usage involves strings for matching and replacement, you can pass functions for any argument to achieve fine-grained control.

Internally, all strings and regexes are converted to functions. Passing a function directly gives you access to the raw context pipeline.

Anatomy of a use() call

appRouter.use(Matcher, ...Middleware);

appRouter.use(
    // Argument 1: The Matcher
    // Can be a string '/path', regex /path/, or function
    (ctx) => {
        // Custom matching logic
        if (ctx.path.includes('secret') && user.isAdmin) {
            return ctx; // Match!
        }
        return null; // No match, try next route
    },

    // Argument 2: Middleware / Logic
    (ctx) => {
        console.log('Secret route accessed');
        // Modify the context for the next handler
        return { ...ctx, path: '/admin/secret.html' };
    }
    // Note: If no implementation is provided and contentEl is set, 
    // the router automatically appends a fetch handler for the final path.
);

API Reference

LightviewRouter.router(options)

Creates a new router instance.

Options:

Built-in Middleware

localeHandler(ctx)

Extracts locale prefixes (e.g., /en/about) from the path. It modifies the context by stripping the prefix (/about) and adding a locale property.

Usage: Add it early in your chain so subsequent handlers see the clean path.

import { localeHandler } from '/middleware/locale.js';

appRouter.use('/*', localeHandler);

// If user visits /en/about:
// 1. localeHandler strips /en/, sets ctx.locale = 'en'
// 2. Next handlers see ctx.path = '/about'

markdownHandler(ctx)

Intercepts requests for .md files, fetches the content, parses it using marked.js (loaded on demand), and renders the resulting HTML.

Returns: A Response object (stops the route chain).

import { markdownHandler } from '/middleware/markdown.js';

// Handle all markdown files
appRouter.use('/*.md', markdownHandler);

notFound(options)

A final middleware that handles 404 errors when no other route matches.

Usage: Add it as the very last route in your configuration.

import { notFound } from '/middleware/notFound.js';

appRouter.use(notFound({
    // contentEl: inherited from router options,
    html: '<h1>404 Not Found</h1>' // Optional custom HTML
}));

router.use(pattern, ...handlers)

Registers a route or middleware.

Pattern types:

router.navigate(path)

Programmatically navigate to a new path.

router.navigate('/profile/settings');

Context Object

Handlers receive a context object containing:

Advanced Configuration

The router() constructor accepts an options object for lifecycle hooks and error handling.

const appRouter = LightviewRouter.router({
    contentEl: document.getElementById('app'),

    // Lifecycle: Called when navigation triggers (e.g., click or back button)
    // Good for starting loading spinners
    onStart: (path) => {
        document.getElementById('spinner').style.display = 'block';
    },

    // Lifecycle: Called AFTER auto-render completes
    // Good for post-render logic like analytics or scroll position
    onResponse: (response, path) => {
        document.getElementById('spinner').style.display = 'none';
        window.scrollTo(0, 0);
        // Run additional logic here (analytics, etc.)
    }
});