Server-render a Laravel and Vue Inertia app

Public pages that need HTML on the first response use the SSR bundle and a process that stays up. An app that is entirely behind login can skip it.

For your AI agent

Copies a prompt. Paste it into the AI agent you already have open.

Read https://laravel-vuejs.space/inertia-ssr.md
Public pages that need HTML on the first response use the SSR bundle and a process that stays up. An app that is entirely behind login can skip it.
Do not add the flag on a Node host.
Do not defer the title you wanted Google to see.
Show the diff. Do not commit.

Next: Log in from a Vue SPA

On this page · 9 sections
  1. When you need SSR
  2. The commands
  3. window is not there
  4. What to render
  5. Where this stops
  6. Recap
  7. FAQ
  8. Next steps
  9. Sources

Your public post list shows a blank page to link previews and slow phones until JavaScript loads. Inertia SSR sends real HTML on the first response. Here is how to build it, run it, and keep your Vue code from crashing it.

TL;DR

  • Ship npm run build:ssr (not just npm run build), then keep php artisan inertia:start-ssr running in production.
  • In development, npm run dev already server-renders through the Inertia Vite plugin.
  • Read window and document only in onMounted. An app entirely behind login can skip SSR.

What you'll learn

  • Which build and Artisan commands SSR needs
  • Why window crashes the server render, and the fix
  • Which pages are worth server-rendering

Who it's for: Laravel + Vue developers with public Inertia pages

Applies to: Laravel 13 · Vue starter kit · inertia-laravel 3 · @inertiajs/vue3 + @inertiajs/vite 3.9

When you need SSR#

Inertia SSR runs your Vue page in Node and sends HTML as the first response. Use it when a public page has to be readable without waiting for JavaScript. A tool that is entirely behind login does not need that HTML. The browser loads the client bundle after the redirect to login either way.

The host process that keeps SSR running is the deploy guide. This page is the build and the Vue code that crashes it. The choice of Inertia versus Nuxt for a public site is the stack guide. Nuxt is the other way to get HTML. You do not run both for the same pages.

The commands#

Production SSR is a second bundle. npm run build writes only the browser files in public/build. It does not refresh bootstrap/ssr. Ship this instead:

bash
npm run build:ssr

Locally, npm run dev and composer run dev already server-render through the Inertia Vite plugin. The current Vue starter kit has no dev:ssr script, and you do not run inertia:start-ssr during development.

In production the process has to stay up:

bash
php artisan inertia:start-ssr
php artisan inertia:check-ssr
php artisan inertia:stop-ssr
CommandWhat it does
npm run build:ssrBuilds the browser bundle and the server bundle in bootstrap/ssr
php artisan inertia:start-ssrStarts the SSR server (keep it supervised)
php artisan inertia:check-ssrConfirms the SSR server answers
php artisan inertia:stop-ssrStops it, for example after a deploy

inertia:check-ssr is how you confirm the process answered before you blame the page. inertia:start-ssr accepts --runtime=bun when you are actually running Bun. Do not add the flag on a Node host.

Gotcha: SSR doesn't make public/hot safe

public/hot still must not be on the server. SSR does not make a dev server safe to deploy.

window is not there#

The first render has no document and no window. A line in <script setup> that reads window.innerWidth throws during that render. Inertia then falls back to the client-only page. Read the browser inside onMounted, or guard it.

Crashes on the server, then Runs in the browser only:

resources/js/pages/posts/Index.vuetypescript
import { ref } from "vue";

const width = ref(window.innerWidth);
resources/js/pages/posts/Index.vuetypescript
import { onMounted, ref } from "vue";

const width = ref(0);

onMounted(() => {
  width.value = window.innerWidth;
});

Heads up: Pinia and SSR

Pinia has the same constraint. Create the store inside the existing withApp callback, not at module scope, so two SSR requests do not share one store. That callback is the Pinia guide. A manual setup callback turns off the Vite plugin’s SSR wiring.

Check the HTML, not the screen

bash
curl -s https://your-app.test/posts | grep -o "<h1[^>]*>[^<]*"

With SSR working, the heading is in the first response. Without it, you only get the empty app container.

What to render#

SSR the public post list if search engines or link previews need the titles in the HTML. Leave the settings screen as a normal Inertia visit. You do not have to server-render every component because one marketing page needed it. The SSR server renders the first visit of each page you built the bundle for. It is a process you operate, next to the queue worker.

Deferred props still arrive after the HTML. They are not in the first document. Partial reloads explain that request. Do not defer the title you wanted Google to see.

Where this stops#

The supervise step on a VPS, php artisan optimize, and the queue are the deploy guide. This page does not pick a host.

Recap#

  • SSR is for public pages that need HTML on the first response.
  • Build with npm run build:ssr, run inertia:start-ssr under a supervisor, verify with inertia:check-ssr.
  • Touch window/document only in onMounted; create Pinia inside withApp.
  • Deferred props are never in the first HTML.

FAQ#

Do I need to run inertia:start-ssr in development?

No. With the Inertia Vite plugin, npm run dev and composer run dev already server-render.

Why does my page render client-side only even with SSR on?

Something threw during the server render, often code that reads window or document in <script setup>. Inertia falls back to the client-only page. Move that code into onMounted.

Does npm run build include the SSR bundle?

No. npm run build only writes public/build. Use npm run build:ssr to refresh bootstrap/ssr too.

Do apps behind a login need SSR?

Usually not. Search engines and link previews never see those pages, and users load the client bundle after login anyway.

Next steps#

Sources#

Found this useful?

Ask questions, share what you built.

Join thousands of Laravel and Vue developers in our Facebook group, or get new posts in your Telegram feed. Sharing this article helps too.

Keep reading

Newsletter

Get the best of Laravel & Vue, once a month.

One short email with new guides, release notes worth reading and hand-picked community links. No spam, unsubscribe any time.