Client-side routing

Turn it on and links stop reloading the page: the next document is fetched, its markup swapped in, and its bindings started. Pages are prefetched when the pointer reaches a link, so a click usually has nothing left to wait for.

Turning it on

Add "router": true to your project's package.json. It is off by default, because changing how every link behaves is not something a project should get without asking.

package.json
{
  "name": "my-app",
  "type": "module",
  "router": true
}

Nothing else changes. Keep writing ordinary <a href="/about"> links — the router attaches itself to the ones it can handle.

What it does

EventBehaviour
Pointer reaches a linkThat page is fetched and held, so the click is instant
ClickMarkup swapped, title and meta tags updated, scrolled to the top
Back / forwardSwapped back, with the scroll position the browser remembered
Anything unexpectedFalls back to a full page load, so navigation always completes

Links it leaves alone

A link is only intercepted when a plain client-side navigation is clearly what was meant.

left to the browser
<a href="https://example.com">another origin</a>
<a href="/report.pdf" download>a download</a>
<a href="/about" target="_blank">a new tab</a>
<a href="#section">a spot on this page</a>

<!-- and opt out explicitly wherever you need to -->
<a href="/legacy" data-no-router>skip the router</a>

Cmd-click, Ctrl-click and middle-click are left alone too, so opening a link in a new tab keeps working.

What happens to your state

Each page starts fresh. Navigating runs the new page's module from scratch, so its signals begin at their initial values. Nothing is carried across a navigation.

This is the honest consequence of pages being independent documents. If you need a value to survive navigation, put it somewhere that outlives the page — sessionStorage, or the URL itself.

Without JavaScript

Every page is server-rendered and every link is a real <a>. With the script blocked or still loading, the site navigates the ordinary way and nothing is missing. The router is an enhancement on top of something that already works.

Cost

Around 2 KB, loaded once and shared by every page. It is loaded after the page's own module, so a page is interactive before navigation is enhanced.

Knowing when a page changed

A page's own module is re-run on every navigation, so its bindings work without you doing anything. A plain <script> that enhances the markup is different: it ran once when the site first loaded, and the nodes it worked on have just been replaced.

The router announces each swap so such a script can run again.

public/site.js
function enhance() {
  for (const heading of document.querySelectorAll('h2')) {
    heading.id = heading.textContent.toLowerCase().replace(/\s+/g, '-');
  }
}

enhance();
document.addEventListener('azox:navigate', enhance);

The event fires after the new markup is in place and after the page's module has run, so a listener always sees the finished page. event.detail holds url and from, which is enough to tell a section change from a move to another part of the site.

Next