Skip to content

Server-Side Rendering

Server-side rendering (SSR) generates the HTML for an Inertia page on the server before sending it to the browser. Visitors see rendered content immediately rather than waiting for JavaScript to load and execute. SSR improves initial load performance, provides better SEO, and ensures content is accessible to crawlers that do not execute JavaScript.

Stratal renders with React 19 streaming (renderToReadableStream): the document shell (server-resolved SEO tags and Vite CSS) flushes immediately, and the app body streams progressively. This lowers time-to-first-byte compared to buffering the entire page before sending a single byte.

Enable SSR by adding the ssr option to InertiaModule.forRoot():

import { Module } from 'stratal/module'
import { InertiaModule } from '@stratal/inertia'
import rootView from './inertia/root.html?raw'
@Module({
imports: [
InertiaModule.forRoot({
rootView,
ssr: {
bundle: () => import('./inertia/ssr'),
disabled: ['admin/*'], // glob patterns where SSR is skipped
},
}),
],
})
export class AppModule {}
OptionTypeDescription
bundle() => Promise<module>A function that dynamically imports the SSR entry file
disabledstring[]Glob patterns for routes that render client-only (no SSR)

Create a file at src/inertia/ssr.tsx (matching your bundle import). It uses createInertiaSsrApp from @stratal/inertia/ssr, which wires Inertia’s App, head collection, and renderToReadableStream, and returns the render(page) function the module expects:

import { createInertiaSsrApp } from '@stratal/inertia/ssr'
export const { render } = createInertiaSsrApp({
resolve: async (name) => {
const pages = import.meta.glob('./pages/**/*.tsx')
const page = await pages[`./pages/${name}.tsx`]?.()
if (!page) throw new Error(`Page not found: ${name}`)
return page
},
})

The render function receives the serialized page object from the server, resolves the correct React component, and streams the rendered HTML. Stratal pipes that stream into the root template before sending the response.

OptionTypeDescription
resolve(name: string) => Component | Promise<Component>Resolve a page component by name. Typically backed by import.meta.glob.
setup({ App, props }) => ReactNodeOptional wrapper for application-level providers (theme, store, i18n, …).
titleHeadManagerTitleCallbackOptional document-title callback applied to page titles.

Wrap the app in providers with setup:

export const { render } = createInertiaSsrApp({
resolve: async (name) => {
const pages = import.meta.glob('./pages/**/*.tsx')
const page = await pages[`./pages/${name}.tsx`]?.()
if (!page) throw new Error(`Page not found: ${name}`)
return page
},
setup: ({ App, props }) => (
<ThemeProvider>
<App {...props} />
</ThemeProvider>
),
})

Build the SSR bundle with the Inertia CLI:

Terminal window
npx quarry inertia:build --ssr

The build runs in two phases: it bundles the browser assets first, then the worker (including the SSR bundle), inlining the client manifest so the worker can resolve hashed asset URLs at runtime. Run this as part of your deploy step.

Some pages (such as admin dashboards or heavily client-only views) may not benefit from SSR. Use the disabled option to exclude them with glob patterns matched against the request pathname:

InertiaModule.forRoot({
rootView,
ssr: {
bundle: () => import('./inertia/ssr'),
disabled: [
'admin/*', // skip all admin pages
'dashboard', // skip the dashboard page
'reports/**', // skip all nested report pages
],
},
})

Disabled routes render a buffered, client-only document: an empty #app div that the client bundle hydrates.

Disable SSR for a single request inside a controller handler with ctx.withoutSsr():

import { Controller, IController, RouterContext } from 'stratal/router'
import { InertiaGet } from '@stratal/inertia'
@Controller('/tools')
export class ToolsController implements IController {
@InertiaGet('/editor')
async editor(ctx: RouterContext) {
ctx.withoutSsr()
return ctx.inertia('tools/Editor', {
// ... props
})
}
}

This is useful when a handler decides to skip SSR based on runtime conditions rather than a static route pattern.

When SSR is enabled and a non-Inertia request arrives (a full page load rather than an XHR visit):

  1. Stratal calls the SSR bundle’s render function with the page data.

  2. The document shell (head tags plus the opening #app wrapper) is flushed immediately.

  3. React’s renderToReadableStream output is piped into the response as it renders.

  4. The wrapper is closed and the Vite script tags are appended. The browser hydrates the streamed markup with React.

On subsequent Inertia visits (XHR requests), SSR is not involved. The server returns JSON props and the client-side router swaps the page component directly.