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
HTML
<!-- 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><head></code> of any HTML page that you want to be
deep-linkable:
</p>
<div class="code-block">
<pre><code><script src="/lightview-router.js?base=/index.html"></script></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: '<h1>404 Not Found</h1>' // 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>