Client Router: the opt-in SPA layer

The feature

Navigation is the floor: the browser navigates, June ships no router. But some surfaces — a dashboard, a builder, anything with a live connection — need in-memory state to survive a navigation, not reset on every click. For those, flip one switch:

// june.config.ts
export default defineJune({ clientRouter: true });

Now same-origin link clicks become soft swaps: June fetches the next page — the same complete HTML document the server already serves, no special payload format — replaces the page region, re-hydrates that page's islands, and animates the swap with View Transitions. History (pushState/back-forward) just works. There is no client route table and no Flight payload: the full-HTML-per-URL contract is the navigation transport.

Persist a live island

Because June composes layouts into the page itself, a normal swap would tear down everything — including an open websocket. Mark an island persist and the router carries its live node (React state, open connections and all) across the navigation instead of re-creating it:

// app/dashboard/layout.tsx
import { Island } from "@junejs/core/islands";
import { LiveFeed } from "./LiveFeed";

export default function DashboardLayout({ children }: { children: React.ReactNode }) {
  return (
    <>
      <Island name="LiveFeed" component={LiveFeed} persist />  {/* socket stays open */}
      {children}
    </>
  );
}

Pure progressive enhancement

The router only ever adds behavior on top of pages that already work:

  • It degrades. No JS, a failed fetch, or a response it doesn't recognize all fall back to a normal browser navigation — never a broken page.
  • It's race-safe. A navigation-generation token discards a stale response when a newer navigation overtakes it (the classic click-then-back bug).
  • The agent surface is untouched. Every URL is still a complete, projectable document — .md, .json, and /mcp (MCP) are exactly what they were. The router lives only on the human surface.

Off by default. This very site keeps it off — it's documents, so the browser navigates and we ship ~1KB of rules, no router.

Why it matters

The real question was never "does June have an SPA mode" — it's "do you want to carry a client router." For most sites the answer is no, and the Standards floor already feels instant. For the app-like minority that needs state to outlive a navigation, the router is there as one config line — and even then it's an enhancement you can remove, not a runtime your whole site now depends on.