Router Plugin

The router plugin (coralite/plugins/router) is an opt-in single-page application router layered on Coralite's static output. Without it, navigation is the default progressive-enhanced model: plain <a href> links, static HTML per route, and native Same-Document View Transitions between full page loads. When registered, the plugin intercepts same-origin link clicks and browser-history navigations, fetches the target's pre-built static HTML, and swaps only the outlet region (and head tags) in the live browser — with no full reload, and no client-side routing of arbitrary client state.

Registration (Required) #

Every SPA behavior of the router — link interception, popstate handling, guards, head reconciliation — lives inside the plugin's client:init and server:onBeforePageRender hooks, which only execute when the plugin is registered. A data-router-outlet marker without plugin registration is inert: the page behaves as a standard multi-page app.

javascript
Code copied!
  import { defineConfig } from 'coralite'
  import router from 'coralite/plugins/router'
  
  export default defineConfig({
    plugins: [
      router({
        base: '/',                   // String, default '/'
        outlet: 'data-router-outlet', // String, default 'data-router-outlet'
        autoWrap: true,               // Boolean, default true
        outletOnly: false             // Boolean, default false
      })
    ]
  })

Invalid config types throw at plugin instantiation (e.g. Invalid config value for "base").

Configuration Options #

Option Type Default Description
base String '/' Declared base path for the router. Declared in the plugin's config schema but currently carries no behavioral wiring in router.js; routing does not perform base-path stripping or prefixing, so a non-'/' base is not honored — do not rely on it today.
outlet String 'data-router-outlet' The attribute name that marks the SPA swap region. Any element carrying this attribute — or the reserved <router-outlet> tag — is an outlet.
autoWrap Boolean true When false, the server-side render pipeline does not automatically wrap outlet-less pages in a <router-outlet>. The page renders exactly as authored. Fetched documents must still contain an outlet for client-side navigation to succeed.
outletOnly Boolean false When true, link interception is skipped whenever the current document has no outlet at click time. The click falls through to the browser's native navigation. This guard is evaluated before link detection and does not affect router.navigate().

Outlet & Swap Region #

Client Router API #

The router API is injected into component and page client() blocks under the router key — client({ router }) — and is also exposed on window.__coralite__.plugins.router:

javascript
Code copied!
  export function client ({ refs, signal, router }) {
    refs('go').addEventListener('click', () => {
      // Push a new history entry
      router.navigate('/app/dashboard')
    }, { signal })
  }

Guards run in registration order before every client navigation. The return value determines what happens next:

Return value Effect
undefined or true Allow the navigation to proceed.
false Cancel the navigation (no history update, no fetch).
Relative path string (e.g. '/login') Soft redirect: the router re-navigates client-side with replace: true. Nested guard redirects are capped at a depth of 10; deeper chains are aborted (the dev runtime logs Redirect loop detected).
Absolute URL string (http://… / https://…) Hard redirect via window.location.replace(url) — a full document transition to an external (or same-origin) page.

A guard that throws (other than an AbortError) is logged in dev runtimes and cancels the navigation. Each running navigation exposes an AbortSignal to its guards: a newer navigation aborts the previous one, and an aborted navigation unwinds silently.

javascript
Code copied!
  export function client ({ router }) {
    const unregister = router.beforeEach(async ({ to, from, signal }) => {
      if (to.path.startsWith('/admin') && !await isAuthenticated()) {
        return '/login' // soft redirect to /login
      }
      if (to.path === '/private-external-origin') {
        return 'https://external.example.com' // hard redirect, full document load
      }
      if (to.path === '/maintenance-hard-block') {
        return false // cancel: stay on the current route
      }
      // undefined / true: proceed
    })
    // Call unregister() (e.g. on route-specific teardown) to remove the guard
  }

The router dispatches two CustomEvents on window (bubbling and composed) with a detail payload of { from, to }, where both are full path strings (pathname + search + hash):

javascript
Code copied!
  export function client ({ signal }) {
    window.addEventListener('coralite:navigating', (event) => {
      // Cancelable: abort the navigation
      if (hasUnsavedChanges()) {
        event.preventDefault()
      }
    }, { signal })
  
    window.addEventListener('coralite:navigated', (event) => {
      const { from, to } = event.detail
      console.log(`Routed ${from} → ${to}`)
    }, { signal })
  }

Behavior Contract #

Start Building with Coralite!

Use the scaffolding script to get jump started into your next project with Coralite

Copied commandline!