Add SSR to an SPA
Add SSR to an SPA: a worked guide from route registry through to a production build.
Example
Reuse the SPA route registry on the server, serialize its initial data into the document, and switch the browser entry from createSPA to hydrateSPA.
import { hydrateSPA } from '@askrjs/askr/boot';
import { renderToString } from '@askrjs/askr/ssr';
// Server entry
const html = renderToString({ registry, url: request.url });
// Browser entry
await hydrateSPA({ root: document.getElementById('app')!, registry });Goal and architecture
Adding SSR means the same route registry now renders on the server first and hydrates in the browser, instead of mounting cold. The client swaps createSPA for hydrateSPA({ root, registry }), and the server calls renderToString() with your route registry or a RouteRenderOptions object to produce the initial HTML. @askrjs/vite's askrServer({ entry }) plugin is the piece that's new here — it owns wiring your server entry module into the Vite dev/build pipeline.
Implementation
Place exactly one <!--askr-head--> marker inside head and one <!--askr-app--> marker at the application mount. Configure askrServer({ entry: 'src/server/entry-server.ts' }) with a module exporting ServerApp. The plugin validates and composes the document internally, preserving authored head content and inserting the Askr-owned output. Custom document integrations can use the supported createDocumentApp helper from @askrjs/vite/server.
Failure states
On hydrateSPA, set hydrate.verifyMarkup during development to catch cases where server-rendered markup doesn't match what the client would produce — silent hydration mismatches are far more expensive to debug than a loud one. Use renderRouteRequest() for routes with loaders or asynchronous auth resolution; renderToString({ registry, url }) does not run route loaders. Register auth requirements on routes in the shared registry; SSG excludes those routes from prerendering by default. SSR accepts request-local auth and authContext options through RouteRenderOptions, so resolve authentication for each request rather than reusing a cached context.
Verification
View source on a rendered route and confirm the markup is real content, not an empty shell waiting for JS — that's the entire point of doing this work. Throttle the network tab and watch hydration complete without a flash of unstyled or duplicated content. Then disable JavaScript entirely and confirm the initial page is still readable; if it isn't, something in your entry-server module is producing markup the static HTML path never exercises.