SSR & Adapters
เปิดใช้ server
หัวข้อที่มีชื่อว่า “เปิดใช้ server”On-demand rendering รัน component script ของคุณต่อ request จึงอ่าน request-time data ได้ จะรัน server code ต้องติดตั้ง adapter สำหรับ platform เป้าหมายแล้วเพิ่มเข้า config:
import { defineConfig } from 'astro/config';import node from '@astrojs/node';
export default defineConfig({ adapter: node({ mode: 'standalone' }), // or cloudflare(), vercel(), netlify()});เมื่อมี adapter route จะ render on demand ตรงที่ต้องการ (หรือเมื่อคุณ set export const prerender = false บน route) ถ้าไม่มี ฟีเจอร์ on-demand ใด ๆ จะ build ไม่ผ่านด้วย “an adapter is required”
sequenceDiagram participant B as Browser participant S as Astro server (adapter) participant C as Component script B->>S: GET /dashboard S->>C: run script with request context C->>C: read cookies, user, query params C-->>S: HTML + response settings S-->>B: Response (status, headers, body)
output และ prerender ต่อหน้า
หัวข้อที่มีชื่อว่า “output และ prerender ต่อหน้า”มีสอง setting ที่ตัดสินต่อ route ว่าจะ build ครั้งเดียวหรือ render ต่อ request: default ระดับโปรเจกต์ output และ export prerender ของแต่ละหน้าเอง ซึ่งชนะ default เสมอ:
output: 'static'(default) — ทุก route ถูก prerender เป็น static HTML ตอน build เว้นแต่หน้านั้น opt เข้า on-demand ด้วยexport const prerender = false(และมี adapter ติดตั้งไว้)output: 'server'— ทุก route render on demand เป็น default; หน้าจะ opt ออก ไปเป็นไฟล์ static ด้วยexport const prerender = true
---// src/pages/about.astro — always static, even under output: 'server'export const prerender = true;---<h1>About</h1>---// src/pages/dashboard.astro — always on-demand, even under output: 'static'export const prerender = false;const user = Astro.locals.user;---{user && <h1>Hi, {user.name}</h1>}Node adapter สองโหมด: standalone vs middleware พิสูจน์ทั้งคู่
หัวข้อที่มีชื่อว่า “Node adapter สองโหมด: standalone vs middleware พิสูจน์ทั้งคู่”@astrojs/node มีสองโหมดที่ต่างกันจริง สร้าง dist/server/entry.mjs ที่ต่างกันเป๊ะ:
import node from '@astrojs/node';export default defineConfig({ adapter: node({ mode: 'standalone' }), // or { mode: 'middleware' }});Standalone คือ server ที่สมบูรณ์ รันเองได้ — node ./dist/server/entry.mjs เริ่ม listen เองเลย:
$ node ./dist/server/entry.mjs04:47:44 [@astrojs/node] Server listening on http://127.0.0.1:4351Middleware mode ไม่สร้าง listener เลย — entrypoint เดียวกันกลับ export handler ดิบ ๆ ให้เอาไปต่อกับ server ที่คุณมีอยู่แล้ว (Express, Fastify, http เปล่า ๆ):
$ node -e "import('./dist/server/entry.mjs').then(m => console.log(Object.keys(m)))"[ 'handler', 'options', 'startServer' ]พิสูจน์ด้วยการเอา handler ตัวนั้นเป๊ะไปต่อกับ Node http server เปล่า ๆ แล้วยืนยันว่า route จริงยัง resolve ผ่านมันได้:
// host.mjs — a minimal example of what "mounting into an existing server" meansimport http from 'node:http';import { handler as ssrHandler } from './dist/server/entry.mjs';
const server = http.createServer((req, res) => { ssrHandler(req, res, (err) => { if (err) { res.statusCode = 500; res.end(String(err)); } });});server.listen(4351);$ curl -s http://127.0.0.1:4351/api/health{"ok":true,"hasSecret":true}หยิบ standalone มาใช้เมื่อ Astro คือ server ทั้งหมด หยิบ middleware มาใช้เมื่อ output ของ Astro ต้องอยู่ในแอปที่มีอยู่แล้ว (Express server เดิมที่มี route อื่นอยู่, custom TLS termination ฯลฯ) — และจำไว้ว่า middleware mode ไม่ serve static file ให้เองเลย dist/client/ ยังต้อง serve แยกเอง
อ่าน request
หัวข้อที่มีชื่อว่า “อ่าน request”ใน on-demand page หรือ endpoint global object Astro เปิดให้เข้าถึง request:
---export const prerender = false;
const url = Astro.url; // a URL objectconst q = url.searchParams.get('q'); // query paramsconst method = Astro.request.method; // the raw Requestconst token = Astro.cookies.get('session')?.value; // typed cookie accessconst { id } = Astro.params; // dynamic route params---<p>Searching for {q}</p>Astro.requestคือRequestมาตรฐาน — headers, method และawait Astro.request.formData()หรือ.json()สำหรับ bodyAstro.urlคือURLที่ parse แล้ว — pathname และsearchParamsAstro.paramsเก็บ dynamic segment ([id])Astro.cookiesอ่านและเขียน cookie ด้วย API ที่ typed (.get,.set,.delete,.has)
กำหนดรูปแบบ response
หัวข้อที่มีชื่อว่า “กำหนดรูปแบบ response”ปกติหน้าจะ return HTML ของมันด้วย status 200 คุณควบคุม response ผ่าน Astro.response (set status และ header) หรือreturn Response ตรง ๆ:
---export const prerender = false;const user = Astro.cookies.get('session')?.value;
// Not logged in? Redirect.if (!user) return Astro.redirect('/login');
// Set a custom header on the HTML response.Astro.response.headers.set('Cache-Control', 'private, max-age=0');---<h1>Your dashboard</h1>Astro.redirect('/login') return Response แบบ redirect; Astro.response.headers แก้ header ขาออก; และคุณ return new Response(...) เพื่อคุมทุกอย่างเองได้ (หน้า 404, status กำหนดเอง) นี่คือ model Request/Response เดียวกับที่ web platform ทั้งหมดใช้ — Astro ไม่ได้คิดแบบของตัวเองขึ้นมา
astro:env: environment variable ที่ type และเช็คที่ boundary
หัวข้อที่มีชื่อว่า “astro:env: environment variable ที่ type และเช็คที่ boundary”import.meta.env.WHATEVER เปล่า ๆ ไม่มี type และไม่ได้ป้องกัน server secret ไม่ให้ถูกอ่านใน client code astro:env เช็ค schema ทุกตัวแปร และที่สำคัญกว่านั้นคือบังคับว่าแต่ละตัวอ่านได้จากที่ไหน — สองแกนที่เป็นอิสระจากกัน:
import { defineConfig, envField } from 'astro/config';
export default defineConfig({ env: { schema: { PUBLIC_SITE_NAME: envField.string({ context: 'client', access: 'public', default: 'Probe' }), API_URL: envField.string({ context: 'server', access: 'public', default: 'https://api.example.com' }), DB_PASSWORD: envField.string({ context: 'server', access: 'secret' }), }, },});context |
access: 'public' |
access: 'secret' |
|---|---|---|
client |
อ่านได้จาก astro:env/client — inline เป็น literal ลงใน client bundle |
ไม่ใช่ combination ที่ valid — Astro ปฏิเสธไม่ให้ config ไม่มีทางส่ง secret ไปที่ browser อย่างปลอดภัย |
server |
อ่านได้จาก astro:env/server, server-only code |
อ่านได้จาก astro:env/server เท่านั้น ไม่ถูก bundle ไป client เลย |
ตัวแปร client/public ถูก inline เป็น literal จริง ๆ — พิสูจน์ด้วยการ grep client script ที่ compile แล้วของหน้าที่ import PUBLIC_SITE_NAME:
$ grep -o 'var e=.*Probe.*;' dist/server/chunks/*.mjsvar e=`Probe`;document.getElementById(`client-side`).textContent=e;ลอง import module ที่เป็น server-only จาก client <script> แล้ว build ปฏิเสธตรง ๆ — ไม่ใช่ undefined ตอน runtime แต่เป็น compiler error จริง:
<script> import { DB_PASSWORD } from 'astro:env/server'; console.log(DB_PASSWORD);</script>[ServerOnlyModule] The "astro:env/server" module is only available server-side. Error reference: https://docs.astro.build/en/reference/errors/server-only-module/และ secret ที่ required แต่ไม่มีค่า set ไว้ก็ไม่ได้กลายเป็น undefined เงียบ ๆ เช่นกัน — route ที่ import มันจะ fail ด้วย diagnostic ของ schema เองทันทีที่ถูกใช้จริง:
$ curl -s http://127.0.0.1:4351/api/health(500)[ERROR] EnvInvalidVariables: The following environment variables defined in `env.schema` are invalid:
- DB_PASSWORD is missing(บังคับให้เช็คนี้เกิดกับทุก secret ที่ config ไว้ไม่ว่าจะถูก import หรือไม่ ได้ด้วย env: { validateSecrets: true } — มีประโยชน์เมื่อต้องการจับ production secret ที่ขาดหายตั้งแต่ใน CI แทนที่จะรอ request แรกที่ต้องการมันจริง ๆ)
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”เมื่อ review rendering/config/env code ที่ generate มา ให้เช็ค:
- config set
output: 'hybrid'หรือเปล่า? ค่านั้นไม่มีอยู่แล้วตั้งแต่ Astro v5 (พิสูจน์แล้วข้างบน) — มันคือศัพท์ค้างจาก training data หรือคู่มือเก่า ไม่ใช่สิ่งที่ compiler ยอมรับ - หน้าอ่าน
Astro.cookies,Astro.clientAddressหรืออะไรที่เกี่ยวกับ request เฉพาะเจาะจง พร้อมกับมี (หรือสืบทอด)prerender = trueด้วยไหม? การอ่านเหล่านั้นต้องการ request จริงที่หน้า prerendered ไม่มีเลย - secret ถูกอ่านผ่าน
import.meta.env.SOMETHINGเปล่า ๆ แทนastro:env/serverหรือเปล่า ซึ่งเสีย schema check และการบังคับ client/server boundary ที่พิสูจน์แล้วข้างบนไปด้วย? - client-side code import จาก
astro:env/serverตรง ๆ โดยคาดว่า “มันแค่จะเป็น undefined” ถ้าใช้ผิด แทนที่จะเป็น build error จริงที่พิสูจน์แล้วข้างบนหรือเปล่า? - สมมติว่า
node ./dist/server/entry.mjsเริ่ม listener เสมอหรือเปล่า? ใน modemiddlewareมันไม่ทำ — พิสูจน์แล้วข้างบนว่ามัน export handler ที่ต้อง mount เอง
🐛 หา bug ในโค้ดนี้
import { defineConfig } from 'astro/config';import node from '@astrojs/node';
export default defineConfig({output: 'hybrid',adapter: node({ mode: 'standalone' }),});เฉลย
output: 'hybrid' ถูกลบออกใน Astro v5 — config นี้ fail validation บน Astro ที่ติดตั้งปัจจุบัน มันไม่ใช่ legacy alias ที่ยังยอมรับเงียบ ๆ พฤติกรรมที่ผู้เขียนต้องการ (“static เป็น default โดยมีบางหน้า on-demand”) คือสิ่งที่ output: 'static' ทำอยู่แล้วเป๊ะ ด้วย export const prerender = false ต่อหน้าที่ต้องการ server — พิสูจน์แล้วก่อนหน้าในบทนี้ วิธีแก้คือ output: 'static' (หรือไม่ต้องใส่ output เลยเพราะเป็น default อยู่แล้ว) พร้อม prerender ต่อหน้าตามที่ต้องการ; output: 'server' คืออีกทางเลือกที่ valid ถ้าส่วนใหญ่ของหน้าควรเป็น on-demand แทน
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”output คือ default ไม่ใช่การแบ่งแยกตายตัว — rendering mode จริงของแต่ละ route คือ prerender ซึ่งชนะเสมอไม่ว่าจะ set เองหรือสืบทอดจาก default ของโปรเจกต์ adapter คือชั้นแปลระหว่าง per-request rendering นั้นกับรูปแบบ request/response ของ runtime หนึ่ง ๆ (http ของ Node, fetch handler ของ Cloudflare ฯลฯ) — นี่คือเหตุผลเป๊ะที่ context.clientAddress, การเขียน Astro.cookies และการอ่าน env สด ๆ ต้องการroute ที่ on-demand จริง ไม่ใช่แค่มี adapter อยู่ที่ไหนสักที่ใน config
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”output: 'static' (default) |
output: 'server' |
|
|---|---|---|
| Override ต่อหน้า | prerender = false opt หน้าเข้าสู่ server |
prerender = true opt หน้าออกไปเป็น static |
| จุดเริ่มต้นที่ดีที่สุด | เว็บส่วนใหญ่ — มีไม่กี่หน้าที่ต้องการ server | แอปที่ส่วนใหญ่ของ route เป็น on-demand |
| ยังต้องมี adapter | ใช่ สำหรับหน้า on-demand ใด ๆ | ใช่ เสมอ |
Node standalone |
Node middleware |
|
|---|---|---|
| Startup | listen เอง พิสูจน์แล้วข้างบน | ไม่มี listener — คุณ mount handler เอง พิสูจน์แล้วข้างบน |
| Static asset | serve ให้อัตโนมัติ | คุณ serve dist/client/ เอง |
| เหมาะกับ | Astro คือ server ทั้งหมด | Astro อยู่ในแอปที่มีอยู่แล้ว |
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”- “
output: 'hybrid'คือ mode สำหรับผสม static กับ on-demand page” พิสูจน์แล้วข้างบน: ค่านั้นไม่มีอยู่แล้วตั้งแต่ v5 —'static'พร้อมprerender = falseต่อหน้าคือ mode นั้นเป๊ะ ภายใต้ชื่อปัจจุบัน - “มี adapter ตัวเดียวก็พอสำหรับ
Astro.clientAddress/cookies/env สด ๆ ที่ไหนก็ได้ในโปรเจกต์” สิ่งเหล่านั้นยังต้องการroute นั้นเจาะจงเป็น on-demand — หน้า prerendered ที่มี adapter ติดตั้งอยู่ที่อื่นในโปรเจกต์ก็ยัง fail พิสูจน์แล้วในบท endpoint - “
node ./dist/server/entry.mjsเริ่ม server เสมอ” แค่ใน modestandaloneเท่านั้น — พิสูจน์แล้วข้างบน modemiddlewareexport handler ที่ไม่มี listener เลย - “secret env var ที่แค่ไม่ได้ set จะกลายเป็น
undefinedตอน runtime เหมือน env var ปกติ” พิสูจน์แล้วข้างบน: secret ที่มี schema แต่ไม่มีค่า throw errorEnvInvalidVariablesจริงทันทีที่ถูกอ่าน ไม่ใช่undefinedเงียบ ๆ