Hosting an Inertia App With SSR: The Setup That Lifts Your Search Visibility

Without SSR, an Inertia app hands search engines a nearly empty page, while SSR delivers full content in the first response. Run the SSR server as a systemd service next to Laravel using 100 to 200 MB, then confirm rendering with curl.
A standard Inertia app sends the browser an HTML page with roughly one element in it: <div id="app" data-page="..."> carrying the page data as JSON, and then React draws the content in the browser. Users do not notice the difference, but search engines do, because the first thing they receive is a page with no headings, no text and no links. Enabling SSR (server-side rendering) fixes this. A Node process runs next to Laravel, receives the page data and returns fully rendered HTML, so the content arrives in the first response before any JavaScript runs. The short setup: build an SSR bundle with vite build --ssr, enable ssr.enabled in config/inertia.php, run the server with php artisan inertia:start-ssr as a systemd service on port 13714, then use curl to confirm the page actually contains its text. Real memory use for the Node process in a mid-sized app is between 100 and 200 MB, and if the service goes down the site keeps working because Inertia falls back to rendering in the browser. This is exactly the setup we run on the CLOUD site itself, and it was the biggest single lever for getting our articles into search results.
What Googlebot actually sees
Googlebot does execute JavaScript, but in two stages. It crawls the page and reads the raw HTML first, then places it in a rendering queue, and rendering can lag by hours or days depending on your crawl budget. During that window your page looks nearly empty to it. The problem is bigger with other crawlers: other search engines, link previews in WhatsApp, X and LinkedIn, and AI crawlers collecting content for answers, most of which never run JavaScript at all.
| Item | SPA without SSR | Inertia with SSR |
|---|---|---|
| First response content | Empty div plus JSON | Full HTML with headings and text |
| Title and meta tags | Added after execution | Present in the first response |
| WhatsApp link preview | Often blank or generic | Correct title and description |
| AI crawlers | Usually miss the content | Read the text directly |
| Time until users see content | After the bundle loads and runs | Immediately, interactive after load |
You can see the difference yourself in Google Search Console. Open the URL inspection tool for any page, choose to view the crawled page, and look at the recorded HTML. Without SSR you will find a single div and a long JSON blob; with SSR you will find every heading, paragraph and internal link. Those internal links matter in particular, because crawlers discover your other pages through them, and if they are missing from the raw HTML new articles can take weeks to be discovered instead of days.
Enabling SSR in the project
If your project started from the official Laravel React starter kit, resources/js/ssr.tsx probably exists already. Otherwise create it to call createInertiaApp with ReactDOMServer.renderToString. Next, update the build script in package.json so it builds both bundles:
"scripts": {
"build": "vite build && vite build --ssr"
}
Then enable SSR in the Laravel config:
// config/inertia.php
'ssr' => [
'enabled' => true,
'url' => 'http://127.0.0.1:13714',
],
After the build you will find bootstrap/ssr/ssr.js, which is what Node runs. Test it by hand first with php artisan inertia:start-ssr and open the site, then turn it into a permanent service.
Running the SSR server as a systemd service
The SSR server is a long-lived process like a queue worker, so if you start it from an SSH window it dies with that window. Here is the unit file we use:
[Unit]
Description=Inertia SSR
After=network.target
[Service]
User=www-data
WorkingDirectory=/var/www/app
ExecStart=/usr/bin/php artisan inertia:start-ssr
Restart=always
RestartSec=2
Environment=NODE_OPTIONS=--max-old-space-size=384
[Install]
WantedBy=multi-user.target
Save it as /etc/systemd/system/inertia-ssr.service and run systemctl enable --now inertia-ssr. The important part comes after every deploy: new code is not picked up automatically because Node keeps the bundle in memory, so add this line to your deploy script right after the build:
php artisan inertia:stop-ssr # systemd restarts it with the new bundle within 2 seconds
Skipping this step causes the strangest bugs. The browser runs the new frontend while the initial HTML comes from the old one, so a hydration mismatch warning shows up in the console and the page may flicker.
Real memory use of the Node process
From our measurements on mid-sized Laravel and React apps (40 to 80 pages):
- At startup the process uses about 70 to 90 MB.
- After hours of traffic it settles between 120 and 200 MB.
- Rendering a single page takes 5 to 30 milliseconds depending on component size.
The --max-old-space-size=384 option in the unit file is a safety ceiling. If memory leaks because a component stores data in a global variable, the process dies at 384 MB and systemd brings it back within two seconds instead of letting it eat the whole server. On a cloud server on the Growth plan (2 vCPU and 4 GB) at 6,900 SAR per year, Laravel with PHP-FPM, MySQL and the SSR server fit comfortably for a site serving tens of thousands of visits a day.
What happens if the SSR service goes down
This is where Inertia shines. If the SSR server does not respond, the Laravel adapter quietly catches the error and returns the regular page with the div and JSON data, and React renders it in the browser as if SSR never existed. Users see no error, but crawlers see empty pages for as long as the outage lasts. So do not settle for the site being up; monitor SSR itself:
# check every 5 minutes from cron
curl -s https://example.sa/ | grep -q '<h1' || echo "SSR down" | mail -s alert ops@example.sa
Another common cause of silent failure is a component that touches window or localStorage directly in its body. Those do not exist in Node, so the render throws and Inertia falls back to client rendering for that page only. Move those calls into useEffect so they run only in the browser.
The effect of SSR on response time
A common question is whether SSR slows the site down. In numbers, rendering adds 5 to 30 milliseconds per request, and the call to the SSR server on 127.0.0.1 adds under a millisecond because it never leaves the machine. In exchange, users see content before the JavaScript bundle loads, and that bundle can be 300 KB or more and take a full second on an average phone with a weak signal, so the page appears noticeably sooner overall. Many people miss this detail: SSR only runs on the first full page load. Navigation inside the app after that happens through Inertia JSON requests that never touch Node, so the SSR server's load is far lower than your page view count.
If your site sits behind a CDN, you can cache public pages such as articles and product pages at the edge for a few minutes, so an article is rendered once and served to hundreds of visitors without touching Node or PHP. Never cache portal pages behind a login, since they belong to each user, and crawlers cannot reach them anyway, so SSR there does no harm but does nothing for search visibility. As traffic grows you can split the SSR server into two processes on different ports, though on most sites we see a single process copes easily because each render takes only a few milliseconds.
Confirming with curl that the page is really rendered
Do not trust what you see in the browser, since it always runs JavaScript. Request the page the way a crawler does:
curl -s -A "Googlebot" https://example.sa/blog/some-post | grep -o '<h1[^>]*>[^<]*' | head -1
curl -s https://example.sa/ | grep -c '<title'
The first command should print the actual article heading, and if it prints nothing the page is not rendered. The second should return exactly 1. A result of 2 means you set a title in both the Blade template and the Head component, a duplicate that confuses search engines. One last tip: compare the response size before and after SSR. An article page usually jumps from about 3 KB to 25 KB or more because the text now lives inside the HTML.
Head tags and structured data
With SSR, Inertia's Head component renders inside the first response, so title, meta description, og:image and canonical links appear exactly as users see them. Put JSON-LD structured data for articles and products in the same component so it ships with every page, and keep any prices in it identical to the page itself, such as the plans on the pricing page, which are listed in SAR including VAT.
Steps to ship it
- Add
vite build --ssrto the build script and enablessr.enabledinconfig/inertia.php. - Create a systemd service with a 384 MB memory ceiling and
Restart=always. - Add
inertia:stop-ssrto the deploy script after the build. - Move any
windowusage intouseEffect. - Check the heading and the single title tag with curl, then add a recurring check every 5 minutes.


