UNPKG

lightview

Version:

A reactive UI library with features of Bau, Juris, and HTMX plus safe LLM UI generation

328 lines (281 loc) 13.8 kB
<!-- SEO-friendly SPA Shim --> <script src="/lightview-router.js?base=/index.html"></script> <div class="docs-layout"> <aside class="docs-sidebar" src="/docs/router-nav.html"></aside> <main class="docs-content"> <h1>Lightview Router</h1> <p class="text-secondary" style="font-size: 1.125rem;"> A lightweight, pipeline-based History API router with middleware support. </p> <h2 id="overview">Overview</h2> <p> 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. </p> <h3>Features</h3> <ul> <li>Pipeline-based routing (middleware style)</li> <li>History API integration</li> <li>Zero dependencies</li> <li>Route parameters <code>/user/:id</code></li> <li>Wildcard support <code>/api/*</code></li> <li>Async handler support</li> <li>Locale prefix handling</li> <li>Markdown rendering</li> </ul> <h2 id="basic-usage">Basic Usage</h2> <p>Initialize the router with a target element and define your routes.</p> <div class="code-block"> <pre><code>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();</code></pre> </div> <h2 id="middleware">Middleware & Pipelines</h2> <p> Lightview Router uses a "chain of responsibility" pattern. When you register a route, you can provide multiple handlers. </p> <p> <strong>Automatic Rendering:</strong> If you provide <code>contentEl</code> 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 <code>contentEl</code>. </p> <p> You can still define manual handlers for advanced logic: </p> <div class="feature-grid" style="margin: 2rem 0;"> <div class="feature-card"> <h3 class="feature-title">➡️ Continue Chain</h3> <p> If a handler returns <strong>null</strong>, <strong>undefined</strong>, or a <strong>context object</strong>, the router continues to the next handler in the chain. </p> <p class="text-secondary" style="font-size: 0.875rem;">Use this for logging, auth checks, or transforming the path.</p> <pre style="margin-top: 1rem;"><code>// Middleware const logger = (ctx) => { console.log('Visiting:', ctx.path); // Returns undefined, so routing continues };</code></pre> </div> <div class="feature-card"> <h3 class="feature-title">🛑 Stop & Return</h3> <p> If a handler returns a <strong>Response</strong> object, the chain stops immediately, and the router considers the navigation complete. </p> <p class="text-secondary" style="font-size: 0.875rem;">Use this for custom API responses or redirects. </p> <pre style="margin-top: 1rem;"><code>// Final Handler const loadPage = async (ctx) => { return await fetch(ctx.path); // Returns Response, stops routing };</code></pre> </div> </div> <h3>Example Pipeline</h3> <div class="code-block"> <pre><code>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 );</code></pre> </div> <h2 id="advanced-usage">Advanced Usage</h2> <h3>Functional Arguments</h3> <p> The <code>use(...)</code> 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. </p> <p> Internally, all strings and regexes are converted to functions. Passing a function directly gives you access to the raw context pipeline. </p> <h3>Anatomy of a <code>use()</code> call</h3> <p> <code>appRouter.use(Matcher, ...Middleware);</code> </p> <ul> <li><strong>Matcher (Arg 0):</strong> Determines if this chain runs. Returns a context object (continue) or null (skip).</li> <li><strong>Middleware (Args 1...n):</strong> Logic that executes if the matcher succeeds.</li> </ul> <div class="code-block"> <pre><code>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. );</code></pre> </div> <h2 id="seo-advantages">SEO Advantages</h2> <p> Lightview Router is designed to be SEO-friendly even in Single Page Applications. By placing a small shim script at the top of your content-rich pages, you can ensure that deep links work correctly and that search engines can crawl your site effectively. </p> <h3>The Shim Approach</h3> <p> Add the following script tag to the <code>&lt;head&gt;</code> of any HTML page that you want to be deep-linkable: </p> <div class="code-block"> <pre><code>&lt;script src="/lightview-router.js?base=/index.html"&gt;&lt;/script&gt;</code></pre> </div> <p> When a user visits a deep link directly (e.g., <code>/docs/router.html</code>), this script executes immediately. If it's not already at the "base" page, it redirects the browser to the base page while preserving the current path in a query string. The router then restores the correct content on the base page. The crawlers can simply process the HTML on the page being visited. </p> <p>This approach ensures:</p> <ul> <li><strong>Indexability</strong>: Each page is a real HTML file that crawlers can find and index.</li> <li><strong>Deep Linking</strong>: Direct links to any page work seamlessly for users.</li> <li><strong>Performance</strong>: The redirect happens before the rest of the page loads, making it nearly instantaneous.</li> </ul> <p> <em>Note: This documentation website uses this exact approach to handle its routing and search engine optimization.</em> </p> <h2 id="api-reference">API Reference</h2> <h3><code>LightviewRouter.router(options)</code></h3> <p>Creates a new router instance.</p> <p><strong>Options:</strong></p> <ul> <li><code>contentEl</code> (HTMLElement): Element to automatically render content into.</li> <li><code>base</code> (string): Base path for the router.</li> <li><code>onStart</code> (function): Callback when navigation begins.</li> <li><code>onResponse</code> (function): Callback when a handler returns a response.</li> </ul> <h2 id="built-in-middleware">Built-in Middleware</h2> <h3><code>localeHandler(ctx)</code></h3> <p> Extracts locale prefixes (e.g., <code>/en/about</code>) from the path. It modifies the context by stripping the prefix (<code>/about</code>) and adding a <code>locale</code> property. </p> <p><strong>Usage:</strong> Add it early in your chain so subsequent handlers see the clean path.</p> <div class="code-block"> <pre><code>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'</code></pre> </div> <h3><code>markdownHandler(ctx)</code></h3> <p> Intercepts requests for <code>.md</code> files, fetches the content, parses it using <code>marked.js</code> (loaded on demand), and renders the resulting HTML. </p> <p><strong>Returns:</strong> A <code>Response</code> object (stops the route chain).</p> <div class="code-block"> <pre><code>import { markdownHandler } from '/middleware/markdown.js'; // Handle all markdown files appRouter.use('/*.md', markdownHandler);</code></pre> </div> <h3><code>notFound(options)</code></h3> <p>A final middleware that handles 404 errors when no other route matches.</p> <p><strong>Usage:</strong> Add it as the very last route in your configuration.</p> <div class="code-block"> <pre><code>import { notFound } from '/middleware/notFound.js'; appRouter.use(notFound({ // contentEl: inherited from router options, html: '&lt;h1&gt;404 Not Found&lt;/h1&gt;' // Optional custom HTML }));</code></pre> </div> <h3><code>router.use(pattern, ...handlers)</code></h3> <p>Registers a route or middleware.</p> <p><strong>Pattern types:</strong></p> <ul> <li><strong>String literal:</strong> <code>'/about'</code> - Exact match.</li> <li><strong>Wildcard:</strong> <code>'/api/*'</code> - Matches any path starting with /api/.</li> <li><strong>Parameter:</strong> <code>'/user/:id'</code> - Captures <code>:id</code> as a parameter.</li> </ul> <h3><code>router.navigate(path)</code></h3> <p>Programmatically navigate to a new path.</p> <div class="code-block"> <pre><code>router.navigate('/profile/settings');</code></pre> </div> <h3>Hash Scrolling</h3> <p> The router fully supports fragment identifiers (hashes). When navigating to a URL with a hash, the router will automatically scroll the corresponding element into view after the content has been fetched and rendered. </p> <div class="code-block"> <pre><code>// Navigates and scrolls to #usage header router.navigate('/docs/router.html#usage');</code></pre> </div> <h3>Context Object</h3> <p>Handlers receive a <code>context</code> object containing:</p> <ul> <li><code>path</code>: The current normalized path.</li> <li><code>params</code>: Object containing named parameters (if any).</li> <li><code>wildcard</code>: Content of the wildcard match (if any).</li> <li><code>contentEl</code>: The main content element (allows middleware to render content or override the target).</li> </ul> <h2 id="advanced-configuration">Advanced Configuration</h2> <p> The <code>router()</code> constructor accepts an options object for lifecycle hooks and error handling. </p> <div class="code-block"> <pre><code>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'; globalThis.scrollTo(0, 0); // Run additional logic here (analytics, etc.) } });</code></pre> </div> </main> </div>