Astro 操作指南
事實驗證至 2026 年 7 月。
Astro 框架開發的全面模式:島嶼架構、內容集合、渲染策略、視圖轉場與多平台部署。
渲染策略決策樹
選擇哪種渲染策略?
│
├─ 內容是否大部分為靜態(部落格、文件、行銷頁面)?
│ ├─ 是 → 內容變更頻率是否低於每日一次?
│ │ ├─ 是 → SSG (output: 'static')
│ │ │ TTFB 最快,可被 CDN 快取,執行階段零成本
│ │ └─ 否 → 混合模式 (output: 'hybrid')
│ │ 預設靜態 + 依路由選擇性啟用 SSR
│ └─ 否 → 每個頁面是否需要個人化內容?
│ ├─ 是 → SSR (output: 'server')
│ │ 依請求動態生成,可感知驗證狀態,即時資料
│ └─ 否 → 混合模式 (output: 'hybrid')
│ 靜態外殼 + 伺服器島嶼處理動態部分
│
├─ 應用程式是否需要即時互動性(儀表板、SPA)?
│ ├─ 是 → 是否為完整的用戶端路由 SPA?
│ │ ├─ 是 → 考慮改用 React/Vue SPA,或使用 Astro + client:only
│ │ └─ 否 → 混合模式 + 島嶼架構
│ │ 在靜態頁面中嵌入互動式島嶼
│ └─ 否 → SSG (output: 'static')
│
├─ 建置時間是否有疑慮(超過 1 萬頁)?
│ ├─ 是 → 使用混合模式搭配隨需渲染
│ │ 預先渲染熱門頁面,對長尾頁面使用 SSR
│ └─ 否 → SSG 即可勝任
│
└─ 是否需要邊緣運算(全球低延遲)?
├─ 是 → SSR + Cloudflare/Vercel Edge 適配器
└─ 否 → SSR + Node 適配器 或 SSG
設定
// astro.config.mjs
import { defineConfig } from 'astro/config';
// SSG(預設)- 所有頁面在建置時預先渲染
// SSG(預設)- 所有頁面在建置時預先渲染
export default defineConfig({
output: 'static',
});
// SSR - 所有頁面在收到請求時渲染
// SSR - 所有頁面在收到請求時渲染
export default defineConfig({
output: 'server',
adapter: cloudflare(), // 或 vercel(), netlify(), node()
});
// 混合模式 - 預設為靜態,依頁面選擇性啟用 SSR
// 混合模式 - 預設為靜態,依頁面選擇性啟用 SSR
export default defineConfig({
output: 'hybrid',
adapter: cloudflare(),
});
---
// 在混合模式下,針對特定頁面選擇不進行預先渲染:
export const prerender = false;
// 在 SSR 模式下,選擇進行預先渲染:
// 在 SSR 模式下,選擇進行預先渲染:
export const prerender = true;
---
島嶼架構快速參考
| 指令 | 水合時機 | 傳送的 JS | 使用案例 |
|---|---|---|---|
client:load | 頁面載入後立即執行 | 完整套件 | 折疊區以上的互動元素(導航列、主視覺 CTA) |
client:idle | 頁面閒置後執行 (requestIdleCallback) | 完整套件 | 折疊區以下的互動元素(評論表單、聊天室) |
client:visible | 滾動到可視區域時執行 | 完整套件 | 頁面深處元件(頁尾小工具、輪播圖) |
client:media | 當媒體查詢條件符合時執行 | 完整套件 | 僅限行動裝置的導航、響應式元件 |
client:only="react" | 立即執行,完全跳過 SSR | 完整套件 | 無法進行 SSR 的元件(Canvas、WebGL) |
| (無) | 永不 - 僅產生靜態 HTML | 零 JS | 靜態內容、卡片、標頭 |
---
import NavBar from '../components/NavBar.tsx';
import CommentForm from '../components/CommentForm.tsx';
import ImageCarousel from '../components/ImageCarousel.svelte';
import MobileMenu from '../components/MobileMenu.vue';
import ThreeScene from '../components/ThreeScene.tsx';
---
<!-- 立即載入 - 關鍵互動元素 -->
<NavBar client:load />
<!-- 頁面閒置後載入 - 非關鍵元素 -->
<CommentForm client:idle />
<!-- 滾入可視區域時載入 - 懶載入 -->
<ImageCarousel client:visible />
<!-- 僅在行動裝置上載入 -->
<MobileMenu client:media="(max-width: 768px)" />
<!-- 僅限用戶端,無 SSR(WebGL 無法在伺服器上執行) -->
<ThreeScene client:only="react" />
內容集合快速入門
定義 Schema
// src/content.config.ts (Astro 5) 或 src/content/config.ts (Astro 4)
import { defineCollection, z, reference } from 'astro:content';
import { glob } from 'astro/loaders';
const blog = defineCollection({
loader: glob({ pattern: '**/*.{md,mdx}', base: './src/content/blog' }),
schema: z.object({
title: z.string(),
description: z.string().max(160),
pubDate: z.coerce.date(),
updatedDate: z.coerce.date().optional(),
heroImage: z.string().optional(),
tags: z.array(z.string()).default([]),
draft: z.boolean().default(false),
author: reference('authors'), // 參照另一個集合
}),
});
const authors = defineCollection({
loader: glob({ pattern: '**/*.json', base: './src/content/authors' }),
schema: z.object({
name: z.string(),
avatar: z.string(),
bio: z.string(),
socials: z.object({
twitter: z.string().optional(),
github: z.string().optional(),
}).optional(),
}),
});
export const collections = { blog, authors };
查詢集合
---
import { getCollection, getEntry } from 'astro:content';
// 取得所有非草稿的部落格文章,依日期排序
const posts = (await getCollection('blog', ({ data }) => !data.draft))
.sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf());
// 取得單一文章
const post = await getEntry('blog', 'my-first-post');
// 解析參照
const author = await getEntry(post.data.author);
// 渲染內容
const { Content, headings } = await post.render();
---
<Content />
內容集合 vs 外部 CMS
| 準則 | 內容集合 | 外部 CMS (Payload 等) |
|---|---|---|
| 內容類型 | 本機 Markdown/MDX、文件、部落格 | 關聯式資料模型 |
| 作者 | 開發者(版本控制) | 編輯者(管理員 UI、多用戶驗證) |
| 驗證 | 在建置時透過 Zod 確保型別安全 | CMS 端 Schema + API 合約 |
| 更新頻率 | 隨網站一同部署 | 獨立於部署之外 |
| API 需求 | 無(建置時查詢) | 供其他消費者使用的 REST/GraphQL |
| 工作流程 | Git PR、簡易審查 | 編輯工作流程、草稿、角色權限 |
經驗法則:從內容集合開始;只有當非開發人員需要在無需部署的情況下發布內容,或當內容確實是關聯式資料時,才考慮使用 CMS。
專案結構參考
project-root/
├── astro.config.mjs # Astro 設定檔
├── tsconfig.json # TypeScript 設定 (extends astro/tsconfigs)
├── package.json
├── public/ # 靜態資源(依原樣複製)
│ ├── favicon.svg
│ ├── robots.txt
│ └── og-image.png
├── src/
│ ├── pages/ # 基於檔案的路由
│ │ ├── index.astro # → /
│ │ ├── about.astro # → /about
│ │ ├── blog/
│ │ │ ├── index.astro # → /blog
│ │ │ └── [slug].astro # → /blog/:slug (動態)
│ │ ├── api/
│ │ │ └── search.ts # → /api/search (API 端點)
│ │ └── [...slug].astro # → 萬用路由/404
│ ├── layouts/
│ │ ├── BaseLayout.astro # HTML 外殼, <head>, 全域樣式
│ │ └── BlogPost.astro # 部落格文章版面
│ ├── components/
│ │ ├── Header.astro # 靜態 Astro 元件
│ │ ├── Footer.astro
│ │ ├── NavBar.tsx # React 島嶼
│ │ └── Counter.svelte # Svelte 島嶼
│ ├── content/ # 內容集合原始檔案
│ │ ├── blog/
│ │ │ ├── post-one.md
│ │ │ └── post-two.mdx
│ │ └── authors/
│ │ └── jane.json
│ ├── content.config.ts # 集合 Schema (Astro 5)
│ ├── middleware.ts # 請求/回應中介軟體
│ ├── styles/
│ │ └── global.css
│ └── lib/ # 共享工具
│ ├── utils.ts
│ └── constants.ts
└── .env # 環境變數
視圖轉場快速參考
---
// src/layouts/BaseLayout.astro
import { ViewTransitions } from 'astro:transitions';
---
<html>
<head>
<ViewTransitions />
</head>
<body>
<slot />
</body>
</html>
轉場指令
<!-- 跨頁面保留元素(維持狀態,避免重新渲染) -->
<audio transition:persist id="player">
<source src="/music.mp3" />
</audio>
<!-- 用於動畫配對的命名轉場 -->
<img transition:name="hero" src={post.heroImage} />
<!-- 自訂動畫 -->
<div transition:animate="slide">內容</div>
<div transition:animate="fade">內容</div>
<div transition:animate="none">無動畫</div>
<!-- 搭配名稱使用 persist(用於多個持續元素) -->
<video transition:persist="media-player" />
生命週期事件
<script>
document.addEventListener('astro:before-preparation', (e) => {
// 在擷取新頁面之前 - 取消導航、顯示載入中
});
document.addEventListener('astro:after-preparation', (e) => {
// 新頁面已擷取,在交換之前
});
document.addEventListener('astro:before-swap', (e) => {
// 自訂 DOM 交換行為
});
document.addEventListener('astro:after-swap', () => {
// DOM 已更新 - 重新初始化腳本
});
document.addEventListener('astro:page-load', () => {
// 頁面完全載入(在初始載入及每次導航時觸發)
// 使用視圖轉場時,應以此事件取代 DOMContentLoaded
});
</script>
返回/前進處理
// astro.config.mjs
export default defineConfig({
prefetch: {
prefetchAll: true, // 懸停時預先擷取所有連結
defaultStrategy: 'hover', // 'hover' | 'tap' | 'viewport' | 'load'
},
});
<!-- 每個連結的預先擷取控制 -->
<a href="/about" data-astro-prefetch>懸停時預先擷取 (預設)</a>
<a href="/blog" data-astro-prefetch="viewport">進入可視區域時預先擷取</a>
<a href="/contact" data-astro-prefetch="load">立即預先擷取</a>
<a href="/external" data-astro-prefetch="false">不預先擷取</a>
部署決策樹
部署到哪裡?
│
├─ 需要邊緣運算 + Cloudflare 生態系統 (KV, D1, R2)?
│ └─ Cloudflare Pages/Workers
│ 適配器:@astrojs/cloudflare
│ 最適合:全球邊緣節點、Workers 綁定、成本效益
│
├─ 需要無伺服器 + Vercel 生態系統 (ISR, 分析)?
│ └─ Vercel
│ 適配器:@astrojs/vercel
│ 最適合:從 Next.js 遷移、圖片最佳化、ISR
│
├─ 需要無伺服器 + Netlify 生態系統 (表單, 身份驗證)?
│ └─ Netlify
│ 適配器:@astrojs/netlify
│ 最適合:JAMstack、內建表單、A/B 測試
│
├─ 需要完整的伺服器控制權(Docker、自訂執行環境)?
│ └─ Node.js (獨立執行或搭配 Express/Fastify)
│ 適配器:@astrojs/node
│ 最適合:自託管、WebSocket、長時間執行的程序
│
└─ 純靜態網站(無需 SSR)?
└─ 任何靜態主機(GitHub Pages、S3、Cloudflare Pages)
不需要適配器,output: 'static'
最適合:部落格、文件、行銷網站
適配器安裝
# Cloudflare
npx astro add cloudflare
# Vercel
npx astro add vercel
# Netlify
npx astro add netlify
# Node.js
npx astro add node
常見陷阱
| 陷阱 | 原因 | 修復方式 |
|---|---|---|
| 水合不一致錯誤 | 伺服器 HTML 與用戶端渲染結果不同(日期、隨機 ID、瀏覽器 API) | 對依賴瀏覽器的元件使用 client:only,或確保渲染結果具有決定性 |
import.meta.env 在用戶端未定義 | 只有 PUBLIC_ 前綴的變數才會暴露給用戶端程式碼 | 重新命名為 PUBLIC_MY_VAR 或透過 props 從伺服器傳遞 |
| SSG 中動態路由回傳 404 | getStaticPaths() 未回傳所有可能的參數 | 確保 getStaticPaths() 回傳所有有效的路徑,或切換至混合模式/SSR |
| 圖片未最佳化 | 使用 <img> 而非 Astro 的 <Image /> 元件 | 從 astro:assets 匯入:import { Image } from 'astro:assets' 並對 src 使用本機匯入 |
| SSR 在沒有適配器時失敗 | output: 'server' 或 'hybrid' 需要一個部署適配器 | 安裝適配器:npx astro add cloudflare (或 vercel、netlify、node) |
| MDX 元件未渲染 | 未將自訂元件傳遞給 MDX 內容 | 透過 <Content components={{ MyComponent }} /> 傳遞元件,或使用 astro.config.mjs 的 MDX 設定 |
| 內容集合 schema 變更未生效 | 型別生成已快取,使用到過時的 .astro 型別 | 執行 astro sync 重新生成型別,重新啟動開發伺服器 |
在 Astro 元件上使用 client:* | 用戶端指令僅適用於框架元件(React、Vue、Svelte) | Astro 元件僅為靜態;將互動部分提取到一個框架元件中 |
document / window 未定義 | 伺服端程式碼無法存取瀏覽器全域變數 | 使用 if (typeof window !== 'undefined') 進行防護,或移至 client:only |
| 樣式在元件之間洩漏 | 使用全域 CSS 而非作用域樣式 | 使用 <style>(在 .astro 中預設為作用域)或刻意使用 <style is:global> |
| 視圖轉場破壞腳本 | DOMContentLoaded 在搭配視圖轉場時僅觸發一次 | 改用 astro:page-load 事件,該事件會在每次導航時觸發 |
| 環境變數在正式環境中遺失 | .env 未載入或平台環境變數未設定 | 在 astro.config.mjs 中使用 envField 進行驗證;在平台管理後台中設定變數 |
正式環境安全檢查清單
對於每次正式環境部署,都需處理以下事項:
- CSP 標頭 - 設定嚴格的
Content-Security-Policy(請參閱references/deployment.md中的中介軟體模式) - 遠端圖片限制 - 強制使用明確的
image.domains/remotePatterns允許清單;絕不從使用者輸入推導圖片 URL(SSRF 風險) - Host 標頭驗證 - 在中介軟體中驗證請求的主機是否符合預期網域(僅限 SSR/混合模式)
- 機密管理 - 在 Cloudflare 上,使用 Workers Bindings (
wrangler secret put),而非內建在程式碼中的環境變數;在其他平台則使用平台的機密儲存庫 - 僅限 HTTPS - 確保所有外部資源(腳本、圖片、字型)都透過 HTTPS 載入
- 輸入驗證 - 在 SSR 環境中清理所有使用者輸入(查詢參數、表單主體、Cookie)
參考檔案
| 檔案 | 內容 | 行數 |
|---|---|---|
references/content-collections.md | Schema 模式、Zod 型別、查詢、MDX、內容層 API、遷移 | ~500 |
references/islands-rendering.md | 島嶼模式、用戶端指令、部分水合策略、效能 | ~300 |
references/view-transitions.md | 轉場 API、生命週期事件、動畫、SPA-like 導航 | ~250 |
references/deployment.md | 適配器設定、Cloudflare/Vercel/Netlify/Node.js 模式、CSP、安全性 | ~400 |
references/project-structure.md | Astro 專案慣例、組織架構、命名規則 | ~200 |