Build an SPA
Build an SPA: a worked guide from route registry through to a production build.
Example
Create one route registry, mount it with createSPA, and keep browser-only services behind lifecycle-owned components.
import { createSPA } from '@askrjs/askr/boot';
import { createRouteRegistry, route } from '@askrjs/askr/router';
const registry = createRouteRegistry(() => {
route('/', DashboardPage);
route('/projects/{projectId}', ProjectPage);
});
await createSPA({ root: document.getElementById('app')!, registry });Goal and architecture
A client-only SPA mounts once, owns routing in the browser, and never touches the server beyond static asset delivery and whatever APIs it calls at runtime. You build a registry with createRouteRegistry(() => { ... }), export it, and hand it to createSPA({ root: '#app', registry }) — the registry is an ordinary value, so the path from route declaration to boot is something you can follow through the imports. @askrjs/vite's askr() plugin is the only build-time piece you need — there's no server plugin, no askrServer() entry, and no document markers to manage.
Implementation
Define routes with route(path, Component) or the nested page()/group()/index() helpers inside your registration function, and read the matched params straight from the component's first argument — Askr infers RoutePathParams<Path> from the literal path string, so route('/users/{id}', UserPage) gives UserPage a typed id: string without you writing the type by hand. State inside components is state()/derive(): read through the getter, write through the setter, no proxies. Wire up <Link> or navigate() for transitions and Outlet() in any layout component that needs to render nested routes.
Failure states
Wrap render-time failures in ErrorBoundary with a fallback — it logs the underlying error and can reset when your app state changes, which matters in an SPA where a bad route param or a malformed API response shouldn't take down the whole shell. Register a fallback(Component) route for unmatched paths so users don't hit a blank screen on a typo'd URL. If a route has an auth policy and the resolver returns deny/redirect, make sure you've actually mounted with auth options on createSPA, or those policies never run.
Verification
Click through every route you registered and confirm the URL bar, browser back/forward, and scroll position all behave — scrollRestoration on SPAConfig is opt-in, so verify it's doing what you expect rather than assuming a default. Force a render-time error in a leaf component and confirm the nearest ErrorBoundary catches it instead of a blank page. Finally confirm the registry you passed to createSPA is the one you actually edited — that is an import you can follow, which is why "routes work in dev but not in the built app" is a much rarer failure with an explicit registry than with an ambient one.