Operate
Inertia SSR & PM2 setup
Pitchbar's admin and marketing pages render through Inertia v3 + React 19.
By default Inertia ships the page state as a JSON payload inside a
<script data-page="app"> tag; React then hydrates that into
DOM on first paint. Crawlers (Pitchbar's own CrawlPageJob,
Google, Bing, social-card scrapers) don't execute JS โ they see the empty
shell and bail at the
script
tag.
Server-side rendering (SSR) fixes this by pre-rendering each Inertia page in a Node process before Laravel returns the HTML. Crawlers, SEO bots, and the first-paint of slow connections all see fully-rendered markup.
How it's wired
resources/js/ssr.tsxโ Vite SSR entry. Mirrorsresources/js/app.tsxbut renders viareact-dom/serverinstead ofreact-dom/client.bootstrap/ssr/ssr.jsโ built artifact, committed to the repo alongsidepublic/build/*so the deploy host never has to runnpm run build:ssr.config/inertia.phpโssr.enabled = true,ssr.url = http://127.0.0.1:13714. Octane / FrankenPHP proxies each Inertia response through this URL.ecosystem.config.cjsโ declares thepitchbar-ssrPM2 process alongsidepitchbar-queue.
One-time PM2 setup (per server, ever)
Pitchbar already uses PM2 for the queue worker. Add the SSR process to the same daemon:
cd /var/www/html
pm2 reload ecosystem.config.cjs
pm2 save
pm2 reload picks up the new pitchbar-ssr entry
and starts it. pm2 save snapshots the process list so PM2
restores it on server reboot (assuming pm2 startup was run
when the queue worker was first installed โ if not, run that once too).
Verify the process is running:
pm2 status pitchbar-ssr
# expect: status=online, watching=enabled
curl -s http://127.0.0.1:13714/health
# expect: HTTP 200
Every future deploy
Just git pull. Nothing else.
The PM2 entry watches bootstrap/ssr/ssr.js for mtime changes.
When git pull replaces the committed bundle, PM2 restarts the
Node process automatically. No pm2 reload, no manual restart,
no SSH-and-touch-something step.
How to verify SSR is actually rendering pages
curl -sk -A "Mozilla/5.0" https://YOUR-DOMAIN/integrations \
| grep -oE 'data-server-rendered'
If you see data-server-rendered printed, SSR is working โ
Inertia stamps that attribute on the root <div> only
when the Node renderer answered successfully. If you see nothing, the
Node process either isn't running or is crashing per request โ check
storage/logs/pm2-ssr-error.log.
Graceful fallback when SSR is down
If pitchbar-ssr stops responding (Node process died, port
13714 closed), Inertia falls back to client-side rendering automatically.
Marketing pages will still load for browsers โ but crawlers and social
bots will go back to seeing the JS shell. PM2's autorestart
+ max_restarts: 50 recovers from process-level crashes
without intervention. If the process refuses to start at all, the
error log is the first place to look.
When NOT to commit a stale bundle
Whenever you edit a file under resources/js/,
resources/css/, or any Inertia page, rebuild and re-stage
the SSR bundle in the same commit:
npm run build
npm run build:ssr
git add public/build bootstrap/ssr resources/
Forgetting this means the client-side bundle has new code but the SSR bundle still serves the old version โ first-paint markup diverges from the eventual hydrated DOM and React 19 will throw a hydration mismatch warning. The Architecture page has more on the repo-as-deploy-artifact rule.
Local development
During npm run dev, the Inertia Vite plugin handles SSR
inline without a separate Node process. PM2 is for production only.
To smoke-test SSR locally against the production-style bundle:
npm run build:ssr
node bootstrap/ssr/ssr.js &
curl -sk https://pitchbar.test/integrations | grep data-server-rendered