Skip to content

Build1 publisher3 min readPublished

Laravel serves an empty #app with a 200 when the Inertia SSR process stops answering

Inertia's fallback to client-side rendering is deliberate, so a dead SSR process hides behind a 200. A Bunfolio developer traced two indexed pages out of a whole sitemap back to twenty-two bare route() calls.

The Engineer · Build desk

Illustration accompanying Laravel serves an empty #app with a 200 when the Inertia SSR process stops answering

What happened

  • A Bunfolio site running Laravel 12, Inertia and React 19 rendered normally in browsers while Google Search Console had crawled two pages of the entire sitemap and left dozens of posts unindexed.
  • The Node SSR server had stopped returning HTML, and Laravel's response to that is to serialise the page props into a data-page attribute on an empty div and return the page with a 200.
  • php artisan inertia:check-ssr confirmed that something was answering the configured SSR URL while production pages were still being served with an empty #app.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

  • constraint Status-code monitors and screenshot checks cannot tell a server-rendered page from the fallback, so any useful assertion has to move into the response body before the JavaScript runs.
  • exposure Every client without a JavaScript engine gets a div with no prose in it, so crawlers, link unfurlers and HTTP-library fetchers see a different site from the one the team is looking at.
  • decision Teams running start-ssr have to put it under a supervisor and add a body-content check, because a port test clears two of the three ways this breaks.
  • cost The cost lands weeks after the process died, and it is paid by the site owner in pages that were crawled once and then left out of the index.

The shim the developer put at the top of ssr.jsx imports route from ziggy-js and assigns it to global.route, with the config url read from process.env.APP_URL at run time [12]. The run-time read is deliberate. The post says taking the host from the value baked into ziggy.js at build time leaves the markup React hydrates against different from what the client would have produced [13].

Node has no global route(), and the @routes Blade directive only defines one in the browser, so twenty-two call sites threw route is not defined during server render [10]. Inertia handled a bundle that throws exactly as it handles a process that is not there, and fell back to the client [11]. One definition in ssr.jsx covers all twenty-two of those calls [19].

php artisan inertia:check-ssr opens a connection to the configured SSR URL, confirms something answers, and prints "Inertia SSR server is running." [14]. The post lists three ways to end up with an empty #app, and only one of them leaves the port silent: a process someone started by hand over SSH that died when the shell closed [9]. A bundle that throws at render time answers the port, and so does a process running the wrong build, so a connection test can detect one failure mode out of three [18]. The developer says they had a green check-ssr and an empty #app in production at the same time [15].

Uptime checks passed and every status code was 200 [8]. The screenshots were perfect too, because the failure exists only in the part of the response body that a browser immediately overwrites [8].

The check that would have caught it is an HTTP fetch with no JavaScript engine anywhere in it. The post's loop walks five paths, cuts each response at <div id="app">, strips the tags out of everything after it, and counts the characters left [16]. A server-rendered page has prose after that div; the fallback has a data-page attribute and nothing else [4].

What made the empty body expensive at Bunfolio rests on a claim about Google, and it is the author's reading rather than a measurement: Google does render JavaScript, but rendering is queued and budgeted separately from crawling, and on a new site with essentially no inbound links you should assume that budget is close to zero [7]. For the indexing damage to transfer, your site needs to be in that same position [7]. On a domain with crawl history the rendering queue may well reach the pages, and the body served is still an empty div with the props in an attribute [4]. The clients that never recover are the ones with no JavaScript engine at all, including link unfurlers and anything reading the page with an HTTP library [6].

What to watch

  • Whether Inertia adds a way to log or fail loudly when the SSR POST errors, instead of falling back to the client silently.
  • Whether check-ssr gains an assertion on rendered body content, not just a connection to the configured URL.
  • The post does not report whether Search Console moved those pages out of Discovered - currently not indexed after the shim shipped.
Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories