Laravel · Inertia

Laravel Inertia SSR — A Field Guide

Inertia renders on the client by default, which is fine until a crawler or a link preview needs to read the page. Here's how to add server-side rendering to a Laravel + Vue Inertia app — the entry file, the build, the Node server, and the parts that bite in production.

John Kihiu9 min read

Server-side rendering (SSR) is the piece of an Inertia app people reach for too late — usually the week a marketing page needs to rank, or a client pastes a link into WhatsApp and the preview comes back blank. Inertia's default is client-side rendering: the first response is a near-empty HTML shell plus a JSON data-page blob, and Vue paints the screen once the bundle loads. That is perfect for an authenticated dashboard and quietly useless for anything a crawler or a link unfurler needs to read. SSR closes that specific gap and nothing else.

What SSR actually changes

With SSR enabled, Laravel hands the initial page props to a small long-running Node process, which renders your Vue components to a real HTML string and returns it inside the first response. The browser shows meaningful content immediately, then the client bundle hydrates that markup and takes over navigation. Nothing about your controllers, routes, or Inertia::render() calls changes — you are adding a second render target, not rewriting the app.

Be honest about whether you need it. If every route sits behind authentication, SSR buys you a slightly faster first paint and little else, at the cost of a Node process to babysit. If you have public marketing pages, articles, product listings, or anything that must produce link previews and be indexed, SSR is the difference between working and not.

The SSR entry point

SSR needs its own entry file that mirrors app.js but renders on the server. It wraps createInertiaApp in createServer, which is the process Laravel talks to:

JavaScript · resources/js/ssr.js
import { createInertiaApp } from '@inertiajs/vue3'
import createServer from '@inertiajs/vue3/server'
import { renderToString } from 'vue/server-renderer'
import { createSSRApp, h } from 'vue'

createServer((page) =>
  createInertiaApp({
    page,
    render: renderToString,
    resolve: (name) => {
      const pages = import.meta.glob('./Pages/**/*.vue')
      return pages[`./Pages/${name}.vue`]()
    },
    setup({ App, props, plugin }) {
      return createSSRApp({ render: () => h(App, props) }).use(plugin)
    },
  }),
)

The one rule that catches everyone: this file runs in Node, not a browser. There is no window, no document, no localStorage. Any component or third-party library that touches those at import or setup time will crash the render — guard browser-only code inside onMounted, which only runs after hydration on the client.

Wiring Vite and the build

Register the SSR entry with the Inertia Vite plugin and let it own the port and clustering:

JavaScript · vite.config.js
import inertia from '@inertiajs/vite'
import laravel from 'laravel-vite-plugin'
import vue from '@vitejs/plugin-vue'
import { defineConfig } from 'vite'

export default defineConfig({
  plugins: [
    laravel({ input: ['resources/js/app.js'], refresh: true }),
    vue(),
    inertia({
      ssr: { entry: 'resources/js/ssr.js', port: 13714, cluster: true },
    }),
  ],
})

Then compile both bundles — the client bundle for the browser and the SSR bundle for Node — in one command:

JSON · package.json
{
  "scripts": {
    "build": "vite build && vite build --ssr"
  }
}

vite build --ssr emits bootstrap/ssr/ssr.js, which is exactly what the Node server executes.

Running the server

Bash · build & run
# Compile client + SSR bundles
npm run build

# Start, stop, and health-check the Node SSR server
php artisan inertia:start-ssr
php artisan inertia:stop-ssr
php artisan inertia:check-ssr

The SSR server listens on port 13714 by default, and Laravel proxies each first render to it. Confirm it in config and keep a kill switch for when you need to rule SSR out during an incident:

PHP · config/inertia.php
'ssr' => [
    'enabled' => true,
    'runtime' => env('INERTIA_SSR_RUNTIME', 'node'),
    // Fall back to client-side rendering instead of a 500 when a render fails
    'throw_on_error' => (bool) env('INERTIA_SSR_THROW_ON_ERROR', false),
],
Head tags need one extra directive

Server-rendered pages only carry the title and meta you set through Inertia's <Head> component, and only if your root Blade layout includes @inertiaHead inside its <head>. Miss it and your SSR HTML ships with no title or description — the exact SEO problem SSR was meant to solve.

Production, and the parts that bite

The Node process is long-running, so treat it like a queue worker, not a one-shot command. Supervise it, restart it on every deploy, and check it after boot:

The short version

Turn SSR on for the pages the public and crawlers see; leave it off if the app is entirely behind a login and you only care about paint speed. Add the ssr.js entry, build both bundles, run the Node server under a supervisor on port 13714, and put @inertiaHead in your layout. It is a half-day of setup that pays for itself the first time a shared link shows a real title and description instead of an empty shell.

John Kihiu
Acumatica ERP Developer · Laravel Engineer

Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.