# ⚡ spark-html-router
Declarative client routing for [spark-html](https://www.npmjs.com/package/spark-html) — **no JS config, just markup.** Write your routes as `` blocks and call `router()` once.
```html
```
`router()` mounts the page **once** (chrome + the active route together — every
component's `onMount` fires exactly once), shows the `` that
matches the URL, intercepts same-origin `` clicks for SPA navigation (no full
reload), and tracks Back/Forward. The route templates are inert to the core
runtime, so this is a tiny add-on — the `spark-html` core stays router-free.
## Active links (zero config)
After every navigation the router sets `aria-current="page"` on the `` whose
`href` matches the current route, and clears it from the rest. Highlight the
active link with pure CSS — no `useStore`, no per-link expressions:
```html
```
## Reactive active route
For anything beyond link styling — the document title, analytics, a breadcrumb —
the router also publishes the current path to a built-in `route` store, so any
component can react with `useStore('route')` (no `popstate`/`pushState` wiring):
```html
```
## Focus & scroll on navigation (a11y, zero config)
On a forward navigation the router moves keyboard/screen-reader focus into the
newly rendered view (so users aren't stranded at the top of the old page) and
resets scroll — to the `#hash` target if the URL has one, otherwise to the top.
Back/Forward (popstate) is left alone: the browser restores its scroll position
and focus isn't yanked.
By default the view's root receives focus. To choose a better target (e.g. the
page heading), mark it:
```html
About
…
```
(`[autofocus]` works too.) The router adds a temporary `tabindex="-1"` if needed
and removes it on blur, so nothing lingers in the DOM or the tab order.
## Install
```bash
npm install spark-html-router
```
## API
```js
import { router, navigate } from 'spark-html-router';
await router({ base: '/spark' }); // base = path prefix (e.g. GitHub Pages)
navigate('/about'); // navigate programmatically
```
| Option | Meaning |
|--------|---------|
| `base` | Path prefix the app is served under (e.g. `/spark`). Stripped before matching, added back when navigating. |
| `root` | Mount root (default `document.body`). |
## Routes
- **Exact match** — `route="/about"` matches `/about` (trailing slashes and the
base path are normalized away).
- **Dynamic segments** — `route="/blog/:id"` matches `/blog/42`; the captured
params land on the `route` store. Exact routes win over dynamic ones.
- **Catch-all** — `route="*"` renders for any unmatched path (a 404 page).
- **Default 404** — if the page declares no `route="*"`, the router injects a
minimal built-in not-found view (a 404 heading + a link home), so unknown
URLs never render a blank page. Declare your own `route="*"` to replace it.
With `spark-prerender`, a `404.html` is also generated automatically at
build time (your own `404.html`, e.g. from `public/`, always wins).
```html
Post #{post}
```
Precedence: **exact → dynamic → catch-all**. Navigating between two matches of
the same dynamic route (`/blog/1` → `/blog/2`) re-mounts the route with the new
params.
## Query string — `route.query`
The URL's search params are a plain reactive object on the `route` store — no
manual `location.search` parsing, no popstate wiring:
```html
```
- Reading: `route.query` mirrors `URLSearchParams` as `{ page: "2", q: "hi" }`
(values are strings, like the platform gives them).
- Writing `route.query.page = "3"` updates the URL **in place** via
`replaceState` — shareable state with no navigation and no history entry.
- Setting a param to `null`/`undefined`/`''` removes it from the URL.
- `navigate('/projects?page=2')` works; navigating without a query string
clears `route.query` (the URL is the source of truth).
## Nested routes & layouts
Nest `` blocks to build a persistent layout with swappable
children. A parent route renders whenever the URL is *under* it; the matching
child renders wherever you place the nested templates:
```html
```
The **parent layout is kept alive** across child navigations — its components
aren't re-mounted, so layout state (open menus, scroll, form input) survives.
Only the part of the tree that actually changed is rebuilt. Precedence per level
is still exact → dynamic → longest layout-prefix → catch-all.
## SEO / prerender
Pair it with [`spark-prerender`](https://www.npmjs.com/package/spark-prerender):
it discovers your `` routes at build time and emits one
fully-rendered HTML file per route (`about.html`, `projects.html`, …) plus the
host rewrite rules — so crawlers get real content per URL, and the client
adopts the prerendered route with no flash.
## Notes
- Covers exact routes, dynamic `:param` segments, nested routes/layouts, and a
catch-all. Dynamic routes render on the client (their params aren't known at
build time, so `spark-prerender` skips them); concrete top-level routes
prerender as usual (nested children render on the client). Wildcard splats
(`/docs/*`) are not yet supported.