Ilha Router
@ilha/router is a lightweight, isomorphic router for ilha apps. It runs on the server as a synchronous HTML renderer and in the browser with full signal-driven reactivity. It is a separate package — not bundled with ilha — so you only pay for it when you need multi-page routing.
How it relates to ilha
Every route in @ilha/router is just an ilha island. The router maps URL patterns to islands, runs their loaders, and manages which one is rendered or mounted at any given time. Route context signals (routePath, routeParams, etc.) are standard context() signals, so any island in your app can read the current route without any special wiring.
Install
Quick start
Client-side SPA
Server-side rendering
SSR + hydration (recommended)
The router() builder
router() creates a fresh router instance and resets the route registry. Always call it fresh — never share instances across server requests.
.route(pattern, island, loader?)
Registers a route. Uses rou3 for matching — the same engine as Nitro. First match wins.
Static segments always take priority over :param segments.
.mount(target, options?) — browser only
Mounts the router into a DOM element or CSS selector. Sets up popstate listening and intercepts internal <a> clicks automatically. Returns an unmount function.
.render(url) — server only
Returns a synchronous HTML string for the matched route. Renders <div data-router-empty></div> when no route matches.
.renderHydratable(url, registry, options?, request?) — server only
Async variant of .render() that outputs HTML with data-ilha hydration markers and serialized loader data. Use this in your SSR handler for the full hydration pipeline.
.renderResponse(url, registry, options?, request?) — server only
Structured-envelope variant of .renderHydratable() that returns a discriminated union instead of a raw string, letting your server emit proper HTTP status codes:
.hydrate(registry, options?) — browser only
Convenience method combining .prime(), ilha.mount(), and .mount() in one call. This is the recommended client entry point.
Loaders
A loader is a data-fetching function that runs before a page renders. Its return value is passed as input props to the island.
The loader receives a LoaderContext:
On the server, loaders run inside .renderHydratable(). On the client, navigations fetch loader data from the /__ilha/loader endpoint before mounting the next island.
redirect(to, status?)
Throws a redirect sentinel inside a loader. The router catches it and either issues an HTTP redirect on the server or calls navigate() on the client.
error(status, message)
Throws a loader error sentinel. On the server, use .renderResponse() to intercept it and emit a proper HTTP status code.
composeLoaders(loaders)
Merges multiple loaders into one. All run concurrently via Promise.all. Later loaders win on key collision — the page loader overrides layout loaders for the same key.
Used internally by the Vite plugin. Also available for manual composition.
Navigation helpers
navigate(to, options?)
Programmatically navigate to a path. Updates the URL, history stack, and all reactive signals. Duplicate navigations are no-ops.
prefetch(pathWithSearch)
Prefetches loader data for a path in the background. The result is cached and consumed on the next navigation, making the transition feel instant.
RouterLink calls this automatically on mouseenter for links with the data-prefetch attribute.
Route context
These signals reflect the current route and are safe to read inside any island on both server and client.
useRoute()
Returns reactive accessors for the current route state as a convenience object:
routePath · routeParams · routeSearch · routeHash
The underlying context signals for direct access outside of islands:
isActive(pattern)
Returns true if the current path matches a registered pattern:
Built-in islands
RouterView
The outlet island rendered by .mount() and .render(). Wraps the active page island in <div data-router-view>. Renders <div data-router-empty></div> when no route matches.
RouterLink
A declarative link island. Calls navigate() on click and prefetches loader data on mouseenter.
Opt a specific link out of prefetching with data-prefetch="false".
File-system routing
@ilha/router ships a Vite plugin that scans src/pages/, resolves layout and error boundary chains, and generates a ready-to-use router with zero manual route registration.
Setup
Add .ilha/ to .gitignore.
Directory structure
Filename → pattern mapping
Route groups
Folders wrapped in parentheses — (name) — organise files without contributing a URL segment. Use them for shared layouts, co-located pages, or logical grouping with no effect on the URL.
Layouts
A +layout.ts wraps every page in its directory and all subdirectories. Layouts compose inside-out — nearest layout is innermost.
Page loaders
Export a load function from any page file. The Vite plugin detects it automatically, composes it with any layout loaders, and wires it into the router at SSR time.
Error boundaries
A +error.ts catches rendering errors for pages in its directory. The nearest boundary wins. If it re-throws, the next outer boundary takes over.
Virtual modules
The plugin exposes three virtual modules:
Plugin options
Route sorting
Routes are sorted automatically — no need to order files manually:
- Static paths (
/about) — highest priority - Parameterised paths (
/user/:id) - Wildcard paths (
/**:slug) — lowest priority
Within the same tier, longer segment counts and alphabetical order act as tiebreakers.
SSR + hydration flow
On navigation, the client fetches /__ilha/loader?path=…, receives loader data as JSON, and mounts the next island with hydration markers — no full page reload.
TypeScript
Key exported types: