inertia_wisp_ssr
Server-side rendering (SSR) support for inertia_wisp. Renders Inertia.js pages on the server using a supervised pool of Node.js processes, with automatic fallback to client-side rendering if SSR fails.
Installation
Add inertia_wisp_ssr to your gleam.toml:
gleam add inertia_wisp_ssr
Quick Start
1. Add SSR to Your Supervision Tree
Add the SSR supervisor to your application’s supervision tree:
import gleam/otp/static_supervisor as supervisor
import inertia_wisp/ssr.{SsrConfig}
pub fn start_app() {
let config = SsrConfig(
..ssr.default_config(),
module_path: ssr.priv_path("my_app", "ssr/ssr.js"),
)
supervisor.new(supervisor.OneForOne)
|> supervisor.add(ssr.supervised(config))
// |> supervisor.add(other_children...)
|> supervisor.start
}
2. Create an SSR-Enabled Layout
Create a layout factory once at startup, then use it in your handlers. This example uses nakai for type-safe HTML generation:
import gleam/list
import inertia_wisp/inertia
import inertia_wisp/ssr
import nakai
import nakai/attr
import nakai/html
fn my_layout(head: List(String), body: String) -> String {
html.Html([attr.Attr("lang", "en")], [
html.Head(
list.flatten([
[
html.meta([attr.charset("utf-8")]),
html.meta([
attr.name("viewport"),
attr.content("width=device-width, initial-scale=1"),
]),
],
list.map(head, html.UnsafeInlineHtml),
]),
),
html.Body([], [
html.UnsafeInlineHtml(body),
html.Script([attr.src("/app.js")], ""),
]),
])
|> nakai.to_string
}
// In your main(), create the config and layout factory once at startup:
// let config = SsrConfig(
// ..ssr.default_config(),
// module_path: ssr.priv_path("my_app", "ssr/ssr.js"),
// )
// let layout = ssr.make_layout(config)
// Then pass `layout` through your context to handlers.
pub fn handle_request(req: Request, layout) -> Response {
req
|> inertia.response_builder("Home")
|> inertia.props(my_props, encode_props)
|> inertia.response(200, layout(my_layout))
}
3. Create Your SSR Bundle
Create priv/ssr/ssr.js with a render function that returns { head, body }:
React Example:
import { createInertiaApp } from "@inertiajs/react";
import ReactDOMServer from "react-dom/server";
const pages = import.meta.glob("./pages/**/*.jsx", { eager: true });
export async function render(page) {
return createInertiaApp({
page,
render: ReactDOMServer.renderToString,
resolve: (name) => pages[`./pages/${name}.jsx`],
setup({ App, props }) {
return <App {...props} />;
},
});
}
Vue Example:
import { createSSRApp, h } from "vue";
import { renderToString } from "vue/server-renderer";
import { createInertiaApp } from "@inertiajs/vue3";
const pages = import.meta.glob("./pages/**/*.vue", { eager: true });
export async function render(page) {
return createInertiaApp({
page,
render: renderToString,
resolve: (name) => pages[`./pages/${name}.vue`],
setup({ App, props, plugin }) {
return createSSRApp({ render: () => h(App, props) }).use(plugin);
},
});
}
Svelte Example:
import { createInertiaApp } from "@inertiajs/svelte";
import { render as renderToString } from "svelte/server";
const pages = import.meta.glob("./pages/**/*.svelte", { eager: true });
export async function render(page) {
return createInertiaApp({
page,
resolve: (name) => pages[`./pages/${name}.svelte`],
setup({ App, props }) {
return renderToString(App, { props });
},
});
}
Configuration
Customize the SSR configuration:
import gleam/erlang/process
import gleam/option.{None}
import gleam/otp/static_supervisor as supervisor
import gleam/time/duration
import inertia_wisp/ssr.{SsrConfig}
let config = SsrConfig(
module_path: ssr.priv_path("my_app", "ssr/ssr.js"), // Absolute path to JS bundle
name: process.new_name("my_app_ssr"), // Pool process name
node_path: None, // Use system Node.js (or Some("/path/to/node"))
pool_size: 8, // Number of workers
timeout: duration.seconds(5), // Render timeout
)
// Add to supervision tree
supervisor.new(supervisor.OneForOne)
|> supervisor.add(ssr.supervised(config))
|> supervisor.start
// Create layout factory with custom config
let layout = ssr.make_layout(config)
// Use in handlers
|> inertia.response(200, layout(my_template))
Options
module_path- Absolute path to your SSR JavaScript bundle; usessr.priv_path(app_name, path)to resolve paths relative to your app’s priv directoryname- Pool name for process registration; create withprocess.new_name()(default:process.new_name("inertia_wisp_ssr"))node_path- Custom Node.js executable path, orNoneto use system PATH (default:None)pool_size- Number of persistent Node.js worker processes (default:4)timeout- Maximum time to wait for SSR rendering (default:duration.seconds(1))
Helper Functions
ssr.priv_path(app_name, path)- Resolves a path relative to an OTP application’s priv directory. Use this at startup to get absolute paths that work correctly in Erlang releases.
How It Works
SSR Flow
- Your handler calls
inertia.response()withlayout(template)fromssr.make_layout(config) - The SSR layer attempts to render the page using Node.js:
- Serializes the Inertia page data to JSON
- Calls your
ssr.jsrender()function via the Node.js process pool - Receives
{ head, body }from JavaScript - Passes the result to your template function
- Returns the fully-rendered HTML response
CSR Fallback
If SSR fails (Node.js error, timeout, or invalid response), the system automatically falls back to client-side rendering:
- Logs a warning with the failure reason
- Generates a
<div id="app" data-page="...">element with escaped JSON - Your JavaScript bundle hydrates on the client as normal
This ensures your app remains available even if SSR breaks.
Requirements
- Gleam 1.14+ (compiles to Erlang)
- OTP 27+
- Node.js 22+ with your framework’s SSR dependencies installed
Set
NODE_ENV=productionso the SSR script is cached in memory. Without this, page rendering times will be very slow.
Debugging
DEBUG_SSR=1- Enable verbose error logging in the SSR server