วิธีตั้งค่า SSR ให้ Inertia กับ React ใน Laravel ตั้งแต่เครื่องเราจนขึ้น production

Inertia v3 มี Vite plugin ที่ทำ SSR ให้ตอนพัฒนาโดยไม่ต้องเปิด Node แยก ส่วน production ต้อง build สองชุด รัน inertia:start-ssr ค้างไว้ด้วย supervisor และสั่งรีสตาร์ตทุกครั้งที่ deploy ไม่อย่างนั้น Node จะวาดหน้าด้วยโค้ดเก่า

เผยแพร่
เวลาอ่าน
5 นาที

โพสต์นี้ไล่วิธีตั้งค่าตามที่เว็บนี้ใช้อยู่จริง (Laravel 13, Inertia v3, React 19) ถ้ายังไม่แน่ใจว่า SSR ทำอะไรระหว่าง Laravel กับ Node ลองอ่านวิธีการทำงานของ Inertia SSRก่อนได้ครับ

สิ่งที่ต้องเตรียม

  • Inertia v3 ทั้งฝั่ง Laravel (inertiajs/inertia-laravel) และ React (@inertiajs/react) ตัว v3 ต้องใช้ PHP 8.2, Laravel 11 และ React 19 ขึ้นไป (Upgrade Guide)
  • Node 22 ขึ้นไปบนเซิร์ฟเวอร์ production (เอกสาร SSR) เพราะ SSR server เป็น Node process ที่รันค้างไว้ตลอด
  • เซิร์ฟเวอร์ที่ให้รัน process ค้างไว้ได้ เช่น VPS ที่เราดูแลเอง หรือ Laravel Forge โฮสต์แบบแชร์ที่ใช้ cPanel หรือ DirectAdmin ส่วนใหญ่ไม่ให้รัน process แบบนี้ ถ้าเว็บอยู่บนโฮสต์แบบนั้น ปิด SSR ไว้ก่อนก็ได้ เว็บยังทำงานเหมือนเดิมทุกอย่าง แค่ไม่มี HTML จาก server
ตอนพัฒนา Vite ตอบ SSR เอง ส่วน production ต้องมี Node รันค้างไว้

ระหว่างพัฒนา Laravel ส่งงาน render ให้ Vite ส่วนบน production ส่งให้ Node process ที่ supervisor คอยดูแล

ติดตั้ง Vite plugin ของ Inertia

npm install @inertiajs/vite

ใส่ plugin ลงใน vite.config.js ของเว็บนี้หน้าตาแบบนี้

import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import inertia from '@inertiajs/vite';
import react from '@vitejs/plugin-react';
import tailwindcss from '@tailwindcss/vite';

export default defineConfig({
    plugins: [
        laravel({
            input: ['resources/css/app.css', 'resources/js/app.jsx'],
            ssr: 'resources/js/app.jsx',
            refresh: true,
        }),
        inertia(),
        react(),
        tailwindcss(),
    ],
});

ssr ชี้ไปที่ app.jsx ไฟล์เดียวกับฝั่งเบราว์เซอร์ ไม่ต้องมี ssr.jsx แยก plugin ของ Inertia ห่อ createInertiaApp เป็น server entry ให้ตอน build ฝั่ง server (Vite Plugin Setup)

ปรับ app.jsx ให้ plugin จัดการ SSR

plugin สร้าง server entry และสลับไปใช้ hydrateRoot ให้เอง ก็ต่อเมื่อ createInertiaApp ไม่มี setup ที่เราเขียนเอง ใครอัปเกรดมาจาก v1 หรือ v2 มักมี resolve กับ setup ค้างอยู่ ลองเปลี่ยนเป็น pages

// เดิม: เขียน resolve กับ setup เอง ใช้ createRoot วาดใหม่ทุกครั้ง
createInertiaApp({
    resolve: (name) => {
        const pages = import.meta.glob('./pages/**/*.jsx');
        return pages[`./pages/${name}.jsx`]();
    },
    setup({ el, App, props }) {
        createRoot(el).render(<App {...props} />);
    },
});

// ใหม่: บอกแค่โฟลเดอร์ของหน้า plugin ทำ SSR และ hydrate ให้
createInertiaApp({
    title: (title) => (title ? `${title} · Phattarachai` : 'Phattarachai'),
    pages: './pages',
    progress: { color: '#36407a' },
});

ถ้ายังต้องใช้ setup เอง เช่นครอบ Provider ที่ทำผ่าน withApp ไม่ได้ ก็ต้องเขียน ssr.jsx แยก และเปลี่ยน createRoot เป็น hydrateRoot ตามวิธี Manual Setup

build ทั้งชุดเบราว์เซอร์และชุด server

แก้คำสั่ง build ใน package.json ให้สร้างทั้งชุดเบราว์เซอร์และชุด server

"scripts": {
    "dev": "vite",
    "build": "vite build && vite build --ssr"
}

ชุดเบราว์เซอร์ลงที่ public/build เหมือนเดิม ชุด server ลงที่ bootstrap/ssr/ ซึ่งเป็นไฟล์ที่ build ใหม่ทุกครั้ง ใส่ /bootstrap/ssr ใน .gitignore ไว้ ไม่ต้อง commit

ใช้ <x-inertia::head> ใน root template

v3 มี Blade component แทน @inertiaHead กับ @inertia ข้อดีคือใส่ค่าสำรองได้ ค่าในช่องของ <x-inertia::head> จะแสดงเฉพาะตอนไม่มี SSR หน้าที่ผ่าน SSR จึงไม่มี <title> ซ้ำสองอัน (Title & Meta)

<!DOCTYPE html>
<html lang="th">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    @viteReactRefresh
    @vite(['resources/css/app.css', 'resources/js/app.jsx'])
    <x-inertia::head>
        <title>{{ config('app.name') }}</title>
    </x-inertia::head>
</head>
<body>
    <x-inertia::app />
</body>
</html>

Vite จัดการ SSR ให้ระหว่างพัฒนา

composer dev

หรือ npm run dev ถ้าเปิด PHP ด้วย Herd หรือ Valet อยู่แล้ว ไม่ต้อง build --ssr และไม่ต้องเปิด Node แยก Vite เปิด endpoint /__inertia_ssr ให้ Laravel ส่งงานมา render ด้วยโค้ดล่าสุดที่เพิ่งแก้ (Development Mode)

เช็กว่า SSR ทำงานด้วย curl ดูว่า HTML ที่ได้มี data-server-rendered ไหม (เว็บในเครื่องที่ใช้ HTTPS ของ Herd ใส่ -k ด้วย)

curl -sk https://myapp.test/ | grep -o 'data-server-rendered="true"'

หรือปิด JavaScript ใน DevTools แล้วโหลดหน้าใหม่ ถ้ายังเห็นเนื้อหาครบ แปลว่า HTML มาจาก server แล้ว

ถ้าหน้าไหน render บน server ไม่ผ่าน Vite จะพิมพ์ error ใน terminal พร้อมชื่อ component และบรรทัดที่พัง ส่วนใหญ่เป็นการเรียก window หรือ document ระหว่าง render เรื่องนี้เล่าไว้ในโพสต์ก่อนหน้า

บน production ใช้ supervisor รัน SSR server ค้างไว้

บนเซิร์ฟเวอร์ build ทั้งสองชุดก่อน แล้วสั่ง SSR server ด้วยคำสั่ง artisan

npm ci
npm run build
php artisan inertia:start-ssr

inertia:start-ssr เปิด Node รัน bootstrap/ssr/ ที่ 127.0.0.1:13714 ค่านี้ตรงกับ INERTIA_SSR_URL ที่ Laravel ใช้อยู่แล้ว ไม่ต้องตั้งเพิ่ม และไม่ต้องเปิด port นี้ใน firewall เพราะมีแค่ Laravel บนเครื่องเดียวกันที่คุยด้วย

คำสั่งนี้ต้องรันค้างไว้ตลอด และต้องฟื้นเองเมื่อ process ตาย จึงให้ supervisor ดูแล แบบเดียวกับ queue worker ตัวอย่างนี้ตัดมาจากที่เว็บนี้ใช้

[program:myapp-ssr]
command=/usr/bin/php /var/www/myapp/artisan inertia:start-ssr
directory=/var/www/myapp
user=www-data
environment=PATH="/usr/local/bin:/usr/bin:/bin"
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
redirect_stderr=true
stdout_logfile=/var/www/myapp/storage/logs/ssr.log
sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl status myapp-ssr

ใครใช้ Laravel Forge มีปุ่ม Inertia SSR ในหน้า site ให้เปิด Forge สร้าง daemon และใส่คำสั่งรีสตาร์ตใน deploy script ให้เอง

รีสตาร์ต SSR server ทุกครั้งที่ deploy

Node โหลด bootstrap/ssr/ เข้าหน่วยความจำตอนสตาร์ต build ใหม่ไปแล้ว process เดิมก็ยังวาดหน้าด้วยโค้ดเก่า HTML จาก server กับ JavaScript ในเบราว์เซอร์จึงเป็นคนละรุ่นกัน หน้าที่แก้ไปจะ hydrate ไม่ตรงหรือขึ้นเนื้อหาเก่าจนกว่าจะรีสตาร์ต

ใน deploy script ให้สั่งหยุดหลัง build เสร็จ แล้วปล่อยให้ supervisor สตาร์ตใหม่บน bundle ใหม่

npm ci
npm run build
php artisan optimize
php artisan inertia:stop-ssr --graceful
sleep 5
php artisan inertia:check-ssr
  • inertia:stop-ssr สั่ง Node หยุด supervisor เห็นว่า process ตาย ก็สตาร์ตใหม่ให้ (autorestart=true)
  • --graceful ทำให้คำสั่งไม่ล้มถ้า SSR ยังไม่ได้รันอยู่ เช่น deploy ครั้งแรก มีตั้งแต่ inertia-laravel 3.4 รุ่นก่อนหน้านั้นใช้ php artisan inertia:stop-ssr || true แทน
  • inertia:check-ssr เช็กว่า SSR กลับมาตอบแล้ว (Deployment)

ตั้ง timeout และเก็บ log เมื่อ SSR ทำงานผิดพลาด

ถ้า SSR ล่ม หรือ render หน้าไหนไม่ผ่าน Inertia ส่งหน้าแบบไม่มี SSR ไปแทน เว็บยังใช้งานได้ปกติ เราจึงอาจไม่รู้ตัวว่า SSR หยุดทำงานไปแล้ว สองอย่างนี้ช่วยได้

  • timeout คำขอไป SSR ใช้ค่าเริ่มต้นของ HTTP client ของ Laravel ถ้า Node ค้าง ทุก request ก็ช้าตาม ตั้งแต่ inertia-laravel 3.4 ตั้ง INERTIA_SSR_TIMEOUT (หน่วยเป็นวินาที) ใน .env ให้ตัดเร็ว ๆ แล้วไปใช้หน้าแบบไม่มี SSR แทน
  • event SsrRenderFailed ฟังแล้วเขียน log หรือส่งเข้าระบบเก็บ error ที่ใช้อยู่
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Log;
use Inertia\Ssr\SsrRenderFailed;

public function boot(): void
{
    Event::listen(SsrRenderFailed::class, function (SsrRenderFailed $event): void {
        Log::warning('SSR failed', $event->toArray());
    });
}

ปิด SSR เฉพาะบางหน้าและตอนรัน test

หน้าหลังบ้านไม่ต้องการ SEO หรือ link preview ปิด SSR ให้ path เหล่านั้นได้ใน middleware ของ Inertia (Excluding Routes)

protected $withoutSsr = [
    'admin/*',
];

ส่วนตอนรัน test ไม่ต้องส่งอะไรไปหา Node ปิดใน phpunit.xml แล้วให้ feature test เช็กที่ props แทน HTML

<env name="INERTIA_SSR_ENABLED" value="false"/>

ที่เว็บนี้เราเพิ่ม test อีกตัวที่ render component ด้วย renderToString ใน Vitest แบบไม่มี DOM จะได้รู้ตั้งแต่ใน CI ถ้ามีใครเผลอเรียก window ระหว่าง render ก่อนจะไปพังบน production

อ่านต่อ

ถูกอ้างถึงใน

ล่าสุด

ดูทั้งหมด →