Skip to content

jtdowney/inertia_wisp_ssr

Repository files navigation

inertia_wisp_ssr

Package Version Hex Docs

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.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 });
    },
  });
}

4. Configure Vite for SSR

Vite externalizes dependencies by default during SSR builds. For SSR to work correctly, configure Vite to bundle all dependencies:

// vite.config.js
export default defineConfig({
  ssr: {
    noExternal: false,
  },
});

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.layout(config, _)

// Use in handlers
|> inertia.response(200, layout(my_template))

Options

  • module_path - Absolute path to your SSR JavaScript bundle; use ssr.priv_path(app_name, path) to resolve paths relative to your app's priv directory
  • name - Pool name for process registration; create with process.new_name() (default: process.new_name("inertia_wisp_ssr"))
  • node_path - Custom Node.js executable path, or None to 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

  1. Your handler calls inertia.response() with layout(template) from ssr.layout(config, _)
  2. The SSR layer attempts to render the page using Node.js:
    • Serializes the Inertia page data to JSON
    • Calls your ssr.js render() function via the Node.js process pool
    • Receives { head, body } from JavaScript
    • Passes the result to your template function
  3. 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

Important

Set NODE_ENV=production so 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

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Contributors

Languages