Navigation
Client-side navigation in Pulse requires no configuration. When hydration is active, same-origin link clicks are intercepted automatically — the server renders the new page and returns JSON, and Pulse swaps the content without a full reload. If anything fails, it falls back to standard browser navigation.
How it works
When initNavigation is called (part of the hydration bootstrap), Pulse attaches a click listener to the document. When a same-origin <a> is clicked:
- The default navigation is prevented.
- A fetch request is sent to the new URL with the header
X-Pulse-Navigate: true. - The server renders the page and returns a JSON response instead of full HTML.
- Pulse replaces the current page's root
innerHTMLwith the new content and updatesdocument.title. - The new page's spec bundle is dynamically imported.
mount()is called to bind the new spec's events.- The browser history is updated with
history.pushState.
location.href = url for a standard full-page navigation.initNavigation
initNavigation is called automatically by the hydration bootstrap script. You do not call it manually in application code. It is exported from the runtime for use in custom bootstrap scenarios:
import { mount } from '@invisibleloop/pulse/runtime'
import { initNavigation } from '@invisibleloop/pulse/navigate'
mount(spec, root, window.__PULSE_SERVER__ || {}, { ssr: true })
initNavigation(root, mount)
JSON response shape
When Pulse receives a request with X-Pulse-Navigate: true, it renders the page and returns:
{
"html": "<main>...the page content...</main>",
"title": "New Page Title — Site",
"hydrate": "/dist/new-page.boot-abc123.js",
"serverState": { "key": "value" }
}
| Field | Description |
|---|---|
html | The rendered page content (the output of the view function, without the full document wrapper). |
title | The new page title, set via document.title. |
hydrate | The bundle path for the new spec. null if the page has no hydration. |
serverState | The server data for the new page, used when mounting the new spec. |
Which links are intercepted
Only same-origin links are intercepted. Links with target="_blank", download, rel="external", or any cross-origin href are ignored and behave normally:
<!-- Intercepted by Pulse client navigation -->
<"tok-fn">class="tok-kw">a "tok-fn">href="/about">About</"tok-fn">class="tok-kw">a>
<"tok-fn">class="tok-kw">a "tok-fn">href="/products/42">Product</"tok-fn">class="tok-kw">a>
<!-- NOT intercepted — standard browser navigation -->
<"tok-fn">class="tok-kw">a "tok-fn">href="https://example.com">External</"tok-fn">class="tok-kw">a>
<"tok-fn">class="tok-kw">a "tok-fn">href="/report.pdf" download>Download</"tok-fn">class="tok-kw">a>
<"tok-fn">class="tok-kw">a "tok-fn">href="/admin" "tok-fn">target="_blank">Open in new tab</"tok-fn">class="tok-kw">a>
Browser history
Pulse uses the History API (history.pushState) to update the URL after each navigation. The back and forward buttons work as expected — each history entry corresponds to a page navigation.
When the user navigates back, Pulse receives a popstate event and performs the same fetch-and-swap process for the previous URL.
Navigation without hydration
Client-side navigation only works on pages that have loaded the Pulse client runtime (i.e. pages with a hydrate path). If a user navigates from a hydrated page to a non-hydrated page, Pulse falls back to a full page load.
hydrate entirely and relying on standard browser navigation is simpler and keeps the JS payload at zero.Scroll behaviour
After each client-side navigation, Pulse scrolls to the top of the page — matching the behaviour of a full page load. If the URL includes a hash (e.g. /docs#section), Pulse scrolls to the target element.