B Blengi docs

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. Mirrors resources/js/app.tsx but renders via react-dom/server instead of react-dom/client.
  • bootstrap/ssr/ssr.js โ€” built artifact, committed to the repo alongside public/build/* so the deploy host never has to run npm 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 the pitchbar-ssr PM2 process alongside pitchbar-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