In an Inertia 3 Laravel app, how does server-side rendering produce the HTML for a public pricing page, and which requests does it apply to?
answer
- only the first, full page load
- the root view's directives call the gateway
- POST the page object to /render
- port 13714, head and body back
- failure falls back to client rendering
basics
~20 sOn a full page load, the root Blade view asks Inertia's SSR gateway to POST the page object to a Node or Bun server, which returns head and body HTML; later Inertia visits get JSON only, and any SSR failure falls back to client rendering.
solid answer
~40 sSSR only touches **full page loads**: a request without `X-Inertia` renders the root Blade view, and its `@inertia` / `@inertiaHead` directives (or `<x-inertia::app>` / `<x-inertia::head>`) call the SSR gateway once per request. The gateway POSTs the page object (component, props, URL, version) to the SSR server's `/render` endpoint, `http://127.0.0.1:13714` by default (`inertia.ssr.url`), or to Vite's `/__inertia_ssr` endpoint while `npm run dev` is running hot. The server renders the page component and returns `head` and `body`, which the directives echo into the HTML, and the client adapter then hydrates it. Every later Inertia visit returns JSON and is rendered in the browser. If SSR is disabled, the bundle is missing or the request fails, the view falls back to the page JSON plus an empty root element, and an `SsrRenderFailed` event fires.
code
html · 13 lines<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
@vite('resources/js/app.js')
<x-inertia::head>
<title>{{ config('app.name') }}</title>
</x-inertia::head>
</head>
<body>
<x-inertia::app />
</body>
</html>go deeper
Remember that SSR sends real HTML for the first page load so crawlers see content, and that normal Inertia navigation stays client-side.
Walk the flow: root view directives, the gateway POST to /render on port 13714, head and body echoed, hydration, and the client-rendering fallback.
Reason about failure modes and cost: silent fallbacks, timeouts on the SSR call, browser-only code in components, and the extra process to operate.
Decide whether a product needs SSR at all or only on some sections, weighing crawlability and first paint against operating a second runtime.
## Why an Inertia app needs SSR at all Without server-side rendering, an **Inertia** page's first response is a Blade shell: a root `<div>` and the page object as JSON. The browser downloads the JavaScript bundle, resolves the page component (for example `Marketing/Pricing`) and renders it. A crawler that does not execute JavaScript, or a link-preview bot, sees an empty body. For public marketing pages that must be crawlable, **SSR** renders the same component on a JavaScript server so the first response already contains the pricing table. ## The first request, step by step 1. The browser requests `/pricing`. The request has no `X-Inertia` header, so `Inertia::render('Marketing/Pricing', $props)` returns the **root view** (`app.blade.php` by default) with the page object. 2. The root view contains `@inertiaHead` in `<head>` and `@inertia` in `<body>`, or the Blade components `<x-inertia::head>` and `<x-inertia::app>`. 3. The first of these asks the **SSR gateway** to dispatch. An internal `SsrState` object makes sure the gateway is called **once** per request and shares the result between head and body. 4. The default `HttpGateway` POSTs the page object with Laravel's HTTP client: - in production, to `inertia.ssr.url` plus `/render` (default `http://127.0.0.1:13714/render`); - while Vite runs hot in development, to the Vite dev server's `/__inertia_ssr` endpoint, so no separate SSR process is needed. 5. The SSR server renders the component and answers with JSON holding `head` (an array of tags) and `body` (the rendered root element). 6. `@inertiaHead` echoes the head tags and `@inertia` echoes the body. 7. In the browser, the client adapter **hydrates** the existing markup instead of rendering from scratch. ## Which requests are server-rendered | Request | SSR involved? | |---|---| | first load, refresh, or link opened in a new tab | yes | | a `<Link>` click or `router.visit` (an Inertia visit) | no, JSON page object only | | a form submission and its redirect | no | | a forced full reload after an asset version change | yes, it is a full page load | This is why SSR costs little per navigation: only the entry request pays for it. It is also why SSR mainly helps crawlers and first paint, not in-app navigation speed. ## When SSR is skipped The published `config/inertia.php` sets `ssr.enabled` to true, yet SSR only happens once a bundle and a server exist. The gateway returns nothing, and the view falls back to client-side rendering, when: - `inertia.ssr.enabled` is false, `Inertia::disableSsr()` applies, or the path matches `withoutSsr`; - not running hot, and no SSR bundle is found (for example `bootstrap/ssr/ssr.js` or `bootstrap/ssr/app.js`), while `ensure_bundle_exists` is true; - the HTTP call fails, times out (`inertia.ssr.timeout`) or returns an error. In the fallback, `@inertia` prints `<script data-page="app" type="application/json">` with the page object and an empty `<div id="app">`, which is exactly the non-SSR output. On failures the adapter dispatches an **`SsrRenderFailed`** event you can listen for, and `inertia.ssr.throw_on_error` turns the silent fallback into an exception for test runs. ## What changed in Inertia 3 - **SSR in development** works through the `@inertiajs/vite` plugin: the Laravel adapter detects Vite's hot file and renders through the dev server. - The initial page is always delivered in a `<script type="application/json">` element, not a `data-page` attribute on the root `<div>`. - Blade components `<x-inertia::head>` and `<x-inertia::app>` join the directives; the head component's slot is a fallback printed only when SSR did not render. ## Costs to keep in mind - a long-running Node (or Bun) process to deploy, monitor and restart; - an extra local HTTP round trip on every full page load; - page components must not touch `window` or `document` during render; - props are serialised twice: into the rendered HTML and into the page JSON for hydration.
- Does clicking a <Link> to /features trigger another SSR render?No. The click is an Inertia visit with `X-Inertia`, so Laravel answers with the page object as JSON and the browser renders the new component. Only full page loads go through the root view and its SSR directives.
- How would you notice that production SSR has silently stopped working?Pages still work because the view falls back to client rendering. Listen for `Inertia\Ssr\SsrRenderFailed` and log it, run `php artisan inertia:check-ssr` as a health check, and in tests set `INERTIA_SSR_THROW_ON_ERROR=true` so a failed render throws instead of falling back.
saying these in an interview costs you the question
- Every Inertia visit is rendered on the Node server
- PHP renders the React or Vue components itself
- An SSR failure returns a 500 error page to the visitor
- Development needs inertia:start-ssr running beside npm run dev
- The head and the body each trigger their own SSR request