Project และ .astro Files
โครงสร้างโปรเจกต์
หัวข้อที่มีชื่อว่า “โครงสร้างโปรเจกต์”โปรเจกต์ Astro ทั่วไปมีโครงสร้างเล็ก ๆ ที่เป็น convention ตรงนี้พิสูจน์ด้วยของจริง ไม่ใช่แค่เล่าใหม่ — scaffold จริงจาก npm create astro@latest -- --template minimal บน Astro 7.3.5:
$ npm create astro@latest -- --template minimal --no-install --no-git --yes ./mysite$ find mysite -not -path '*/node_modules*' -not -path '*/.git*' | sortmysitemysite/.vscodemysite/.vscode/extensions.jsonmysite/.vscode/launch.jsonmysite/AGENTS.mdmysite/CLAUDE.mdmysite/README.mdmysite/astro.config.mjsmysite/package.jsonmysite/publicmysite/public/favicon.icomysite/public/favicon.svgmysite/srcmysite/src/pagesmysite/src/pages/index.astromysite/tsconfig.json$ cat mysite/package.json{ "name": "mysite", "type": "module", "version": "0.0.1", "engines": { "node": ">=22.12.0" }, "scripts": { "dev": "astro dev", "build": "astro build", "preview": "astro preview", "astro": "astro" }, "dependencies": { "astro": "^7.3.5" }, "allowScripts": { "esbuild": true }}engines.node คือ >=22.12.0 — verify ตรง ๆ จาก package ที่ publish จริง (npm view astro engines → { node: '>=22.12.0', npm: '>=9.6.5', pnpm: '>=7.1.0' }) Astro จะไม่รันบน Node เวอร์ชันเก่ากว่านั้น มีสองอย่างที่น่าสังเกตใน scaffold แบบ minimal: ไม่มี src/content.config.ts, src/layouts/, หรือ src/components/ เลย — พวกนี้จะโผล่มาเมื่อคุณเพิ่ม content collections และ component เข้าไป ไม่ได้จำเป็นต้องมีตั้งแต่แรก — และ template ตอนนี้ก็มา with AGENTS.md/CLAUDE.md ที่เป็น quick reference สำหรับ coding agent (background dev server, ลิงก์ doc) ไม่ใช่แค่ doc สำหรับคนอ่านอีกต่อไป
โปรเจกต์ที่ใหญ่ขึ้นและโตขึ้นเรื่อย ๆ จะเติมโครงสร้างที่เหลือให้เต็ม:
my-site/├── astro.config.mjs # config: integrations, adapter, output mode├── src/│ ├── pages/ # file-based routing — แต่ละไฟล์คือหนึ่ง route│ ├── components/ # component แบบ .astro (และ framework) ที่ reuse ได้│ ├── layouts/ # เชลล์ของหน้าที่ใช้ร่วมกัน│ ├── content.config.ts # config ของ content collections (build-time)│ ├── live.config.ts # config ของ live collections (ข้อมูล request-time)│ ├── middleware.ts # รันก่อนทุก request│ ├── actions/index.ts # server Actions (defineAction)│ ├── fetch.ts # Astro 7+: advanced routing entrypoint (reserved)│ ├── env.d.ts # ambient types: App.Locals, ImportMetaEnv│ └── styles/└── public/ # static assets ที่ serve ตรง ๆ (favicon, robots.txt)| Path | บทบาท | Reserved ใน Astro 7 ไหม? |
|---|---|---|
src/pages/ |
File-based routing — แต่ละไฟล์กลายเป็นหนึ่ง route | ใช่ — ตัวโฟลเดอร์เองเป็นแกนหลัก |
src/content.config.ts |
ประกาศ content collections แบบ build-time พร้อม Zod schema | ใช่ — ชื่อไฟล์พิเศษ, auto-import ให้ |
src/live.config.ts |
ประกาศ collection แบบ live (ข้อมูล request-time, ต้องมี adapter) | ใช่ — ใหม่ใน 7.0, ชื่อไฟล์พิเศษ |
src/middleware.ts |
onRequest — รันก่อนทุก request |
ใช่ — ชื่อไฟล์พิเศษ |
src/actions/index.ts |
export object server ของ Actions |
ใช่ — path พิเศษ |
src/fetch.ts |
advanced-routing entrypoint, auto-import ถ้ามีไฟล์นี้ | ใช่ — ชื่อ reserved ใหม่ใน Astro 7.0 (ดูข้างล่าง) |
public/ |
copy ไปยัง output ตรง ๆ serve ที่ root โดยไม่ผ่านการประมวลผล | ใช่ — ตัวโฟลเดอร์เอง |
.astro/ |
types ที่ generate ให้ (astro sync) — .astro/types.d.ts, .astro/content.d.ts |
ไม่ใช่ของ user; ห้าม hand-edit เด็ดขาด |
สองโฟลเดอร์ที่เป็นแกนหลัก:
src/pages/คือ file-based routing:src/pages/index.astro→/,src/pages/about.astro→/about,src/pages/blog/[slug].astro→/blog/:slugตำแหน่งของไฟล์ คือ routepublic/ถูก copy ไปยัง output ตรง ๆ — ไฟล์ในนี้ถูก serve ที่ root โดยไม่ผ่านการประมวลผล
src/fetch.ts — ชื่อ reserved ใหม่ใน Astro 7
หัวข้อที่มีชื่อว่า “src/fetch.ts — ชื่อ reserved ใหม่ใน Astro 7”Astro 7.0 เพิ่ม advanced routing เข้ามา และ src/fetch.ts คือชื่อไฟล์พิเศษของมัน — Astro import ไฟล์นี้อัตโนมัติเพื่อ config พฤติกรรม routing เหมือนที่มัน auto-import src/middleware.ts อยู่แล้ว อ้างตรง ๆ จาก v7 upgrade guide ทางการ:
กายวิภาคของไฟล์ .astro
หัวข้อที่มีชื่อว่า “กายวิภาคของไฟล์ .astro”ไฟล์ .astro คือ component format ของ Astro เอง มีสองส่วนคั่นด้วย code fence (---):
---// 1. The component script (frontmatter). Runs on the SERVER only.// JavaScript/TypeScript here: imports, data fetching, props, logic.import Card from '../components/Card.astro';
const title = "Latest posts";const posts = await fetch('https://api.example.com/posts').then((r) => r.json());---
<!-- 2. The template. Becomes HTML. Uses { } to embed JS expressions. --><h1>{title}</h1><ul> {posts.map((post) => <Card title={post.title} />)}</ul>Mental model สำคัญ:
- ทุกอย่าง เหนือ
---ตัวที่สองคือ component script — รันครั้งเดียว บน server ตอน build (หรือแต่ละ request สำหรับหน้า on-demand) ตรงนี้คือที่ที่คุณดึงข้อมูล import component และคำนวณค่า ไม่มีอะไรในนี้ถูกส่งไป browser - ทุกอย่าง ใต้ นั้นคือ template — HTML ที่มี
{ }expression หย่อน JavaScript ลงไป นี่คือสิ่งที่กลายเป็น HTML ที่ render ออกมา
flowchart LR
script["--- frontmatter ---
component script
(server เท่านั้น)"] --> compute["ดึงข้อมูล, คำนวณ props"]
compute --> template["template (HTML + { } expression)"]
template --> html["HTML ที่ render แล้ว"]เพราะ script รันบน server คุณจึง await ที่ระดับบนสุดได้ — fetch จาก API หรือ database ตรง ๆ ใน component โดยไม่ต้องมี useEffect ไม่ต้องมี loading state ไม่ต้องวิ่งไป client รอบใหม่ ข้อมูลอยู่พร้อมแล้วตอนที่ HTML ถูก build
.astro คือ superset ของ HTML
หัวข้อที่มีชื่อว่า “.astro คือ superset ของ HTML”template .astro คือ HTML ที่ valid บวกกับ expression คล้าย JSX ถ้าคุณวาง HTML ธรรมดาลงในไฟล์ .astro ก็ทำงานได้เลย คุณเพิ่มความ dynamic ด้วย { } (expression), {condition && <div/>} (conditional) และ {items.map(...)} (loop) — syntax แบบ expression เดียวกับที่เห็นในตัวอย่าง landing ต่างจาก JSX ตรงที่คุณใช้ชื่อ attribute แบบ HTML มาตรฐาน (class ไม่ใช่ className)
astro.config.mjs กับ .ts
หัวข้อที่มีชื่อว่า “astro.config.mjs กับ .ts”ไฟล์ config เป็น astro.config.mjs, .js, หรือ .ts ก็ได้ — ตัวโหลด config ของ Astro resolve ได้ทั้งสามแบบ (verify ตรง ๆ จาก package ที่ติดตั้งจริง: astro/dist/core/config/config.d.ts เขียนไว้ว่า resolve astro.config.js|mjs|ts) .mjs คือสิ่งที่ scaffold ทุกอันสร้างให้ (ไม่ต้อง compile แยก ทำงานได้ทันที) แต่ .ts ก็รองรับเต็มที่ถ้าอยากได้ config code ที่ type-check พร้อม import จาก module .ts ของตัวเอง — Astro transpile ให้แบบ on the fly ไม่มีความต่างเชิงความสามารถว่า config อะไรได้บ้าง เป็นแค่เรื่อง “อยากให้ตัวไฟล์เองถูก type-check หรือเปล่า”
Env files
หัวข้อที่มีชื่อว่า “Env files”Astro โหลด .env (และ .env.production, .env.development) ผ่านระบบ env ในตัวของ Vite และแบ่งตัวแปรเป็นสองระดับการมองเห็นตามชื่อ:
SECRET_KEY=sk_live_abc123PUBLIC_SITE_NAME=MySite- ไม่มี prefix
PUBLIC_→ ใช้ได้ในโค้ดฝั่ง server เท่านั้น (import.meta.env.SECRET_KEY) - prefix
PUBLIC_→ ถูก inline เข้าไปในโค้ดฝั่ง client ด้วย แบบ static (import.meta.env.PUBLIC_SITE_NAME)
พิสูจน์ด้วย build จริง — <script> (ฝั่ง client) ที่อ่านทั้งสองตัว:
<html><body><script> console.log('secret from client:', import.meta.env.SECRET_KEY); console.log('public from client:', import.meta.env.PUBLIC_SITE_NAME);</script></body></html><!-- the real built <script> content, dist/client/env-test/index.html --><script type="module">console.log(`secret from client:`,void 0),console.log(`public from client:`,`MySite`);</script>ตัวแปรที่ไม่มี PUBLIC_ ไม่ได้รั่วออกไป — แต่ก็ไม่ error ด้วย มันถูกแทนที่แบบ static ด้วย void 0 (undefined) ตอน build เงียบ ๆ snippet ที่ AI generate มาแล้วอ่าน secret จากโค้ดฝั่ง client ไม่ได้ไม่ปลอดภัยเพราะบังเอิญที่นี่ — มันแค่พังเฉย ๆ แบบเงียบ ๆ อีกเรื่องที่เจอจริงคือ: .env ไม่ถูกโหลดเข้าไปใน astro.config.mjs เอง — ใช้ process.env (ที่ shell/CI เติมให้) แทนถ้า config ต้องการค่าตอน evaluate config
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”โครงสร้างโปรเจกต์หรือ config snippet ที่ AI generate มา ควรเทียบกับ scaffold จริงข้างบนอีกที:
- มันสร้าง convention
src/utils/หรือsrc/lib/แล้วบอกว่า “จำเป็นต้องมี” หรือเปล่า? มีแค่src/pages/กับpublic/เท่านั้นที่เป็นแกนหลัก ที่เหลือ (components/,layouts/,lib/) เป็น convention ไม่ใช่สิ่งที่ Astro บังคับ - มันอ่าน env var ที่ไม่มี prefix
PUBLIC_อยู่ใน<script>tag หรือ client component แล้วคาดว่าจะได้ค่าจริงหรือเปล่า? พิสูจน์แล้วข้างบน: มันกลายเป็นundefinedเงียบ ๆ ใน output ที่ build แล้ว ไม่ใช่ secret ที่รั่ว และก็ไม่ error ด้วย — bug ที่แนบเนียนมาก - มันประกาศ
output: 'hybrid'หรือเปล่า? option นี้ถูกลบใน Astro v5 —'static'ครอบคลุมสิ่งที่ hybrid เคยทำ (ดูบท rendering-modes) - มันเพิ่มไฟล์
src/fetch.tsสำหรับอะไรที่ไม่เกี่ยวกับ routing (fetch wrapper, API client) โดยไม่พูดถึงfetchFileเลยหรือเปล่า? ใน Astro 7 ชื่อไฟล์นั้นถูก auto-import สำหรับ advanced routing และ “may cause unexpected errors” ตามที่ upgrade guide อ้างไว้ข้างบน - มันใช้
astro.config.tsแล้วอ้างว่า.mjs“ไม่รองรับ TypeScript config” หรือเปล่า? ทั้งสองแบบเป็น entrypoint ที่ใช้ได้จริงทั้งคู่.tsแค่ทำให้ไฟล์ตัวเองถูก type-check ไม่ได้ปลดล็อกอะไรที่.mjsทำไม่ได้
🐛 หา bug ในโค้ดนี้
---const key = import.meta.env.ANALYTICS_SECRET_KEY;---<script define:vars={{ key }}>fetch('https://analytics.example.com/init', { method: 'POST', body: JSON.stringify({ key }),});</script>เฉลย
ชื่อตัวแปรไม่มี prefix PUBLIC_ เลย ตามกติกาของ Astro มันจึงเป็น server-only — แต่ define:vars serialize ค่า key จาก frontmatter เข้าไปใน scope ของ client <script> โดยตรง นั่นไม่ได้ผ่านการแยก client/server ของ Astro เลย: define:vars รันหลังจากค่าถูก resolve บน server แล้ว ดังนั้นไม่ว่า ANALYTICS_SECRET_KEY จะเป็นค่าอะไร ก็จะถูกฝังลงไปใน HTML ที่ส่งจริง เป็นตัวหนังสือธรรมดา อ่านได้เต็ม ๆ ผ่าน “view source” พิสูจน์ด้วย build จริง (ANALYTICS_SECRET_KEY=sk_live_shouldnotleak ใน .env) — secret ตัวจริงไปโผล่ใน dist/client/analytics-test/index.html:
<!DOCTYPE html><script>(function(){const key = "sk_live_shouldnotleak";
fetch('https://analytics.example.com/init', { method: 'POST', body: JSON.stringify({ key }), });})();</script>(เทียบกับการอ่าน import.meta.env.ANALYTICS_SECRET_KEY ตรง ๆ ใน <script> block โดยไม่มี define:vars — พิสูจน์แล้วก่อนหน้านี้ในบทนี้ว่ามันจะกลายเป็น undefined แทน เพราะ script ฝั่ง client แบบนั้นถูกประมวลผลเป็น module ของตัวเอง มีแค่ env var ที่ prefix PUBLIC_ เท่านั้นที่ถูก inline) วิธีแก้: อย่าส่ง secret ผ่าน define:vars เด็ดขาด ถ้า client ต้องใช้ token จริง ๆ ให้สร้าง token แบบ short-lived ที่จำกัดสิทธิ์บน server แล้วส่ง อันนั้น แทน
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”ไฟล์ .astro คือโปรแกรมสองตัวที่เชื่อมกันด้วย fence --- หนึ่งอัน: โปรแกรมฝั่ง server (frontmatter) ที่รันครั้งเดียวแล้วผลิตข้อมูล กับโปรแกรม template ที่เปลี่ยนข้อมูลนั้นเป็น HTML ไม่มีอะไรใน frontmatter ข้ามไปฝั่ง client เว้นแต่คุณจะตั้งใจส่งให้ <script> เอง — และแม้จะส่งแล้ว ก็มีแค่ค่า env ที่ prefix PUBLIC_ กับ payload ของ define:vars แบบชัดเจนเท่านั้นที่ข้ามไปได้ พิสูจน์แล้วทั้งสองแบบข้างบน (แบบหนึ่งกลายเป็น undefined เงียบ ๆ อีกแบบรั่วเป็นตัวหนังสือธรรมดา)
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”config .mjs |
config .ts |
|
|---|---|---|
| scaffold สร้างให้ | ใช่ เสมอ | ไม่ — ต้อง opt-in เอง |
| type-check ตัวไฟล์ config เอง | ไม่ | ใช่ |
ยัง import module .ts ของตัวเองได้ไหม |
ได้ | ได้ |
| ขั้นตอน build เพิ่ม | ไม่มี | ไม่มี — Astro transpile ให้ |
env var ที่ไม่มี PUBLIC_ |
env var ที่มี prefix PUBLIC_ |
|
|---|---|---|
| โค้ดฝั่ง server | ใช้ได้ | ใช้ได้ |
| โค้ดฝั่ง client | ถูกแทนที่แบบ static ด้วย undefined — พิสูจน์แล้วข้างบน |
inline เป็นค่าจริง |
| ใช้สำหรับ | secret, database URL, API key | feature flag, ชื่อไซต์ที่เปิดเผย, public API base URL |
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”- “ทุกโปรเจกต์ Astro ต้องมี
content.config.ts,middleware.ts, และactions/index.ts” scaffold แบบ minimal จริงข้างบนไม่มีไฟล์พวกนี้เลยสักไฟล์ — พวกมันเป็น ชื่อไฟล์พิเศษที่ Astro รู้จักถ้ามี ไม่ใช่ไฟล์ที่มันบังคับให้มี - “
astro.config.tsปลดล็อก config option ที่.mjsทำไม่ได้” ทั้งสองแบบ resolve ผ่าน loader เดียวกัน.tsแค่เพิ่ม type-check ให้ตัวไฟล์ config เอง - “env var ที่ไม่มี
PUBLIC_เข้าถึงจากโค้ดฝั่ง client ไม่ได้ ดังนั้นถ้าอ้างถึงตรงนั้นจะถูกจับได้ทันที” พิสูจน์แล้วข้างบน: มันไม่ถูกจับเลย — มันกลายเป็นundefinedเงียบ ๆ ใน script ที่ build แล้ว ซึ่งอ่านเหมือน “feature พัง” ไม่ใช่ “build ล้มเหลว” - “ค่าใน
.envใช้ได้ตอนที่astro.config.mjsเองกำลัง evaluate” ไม่จริง — การโหลด.envเกิดผ่าน Vite สำหรับโค้ดแอปของคุณ ไม่ใช่สำหรับไฟล์ config เอง ให้ใช้process.envแทนตรงนั้น