在 Astro 中,把 .md 丟進 src/pages 就能直接產生對應網址。頁面只有三五篇時,這種檔案即路由(file-based routing)非常省事。
但當內容增加到幾十篇時,只靠 src/pages 就會遇到瓶頸:每篇 frontmatter 欄位缺乏約束、檔案路徑硬綁網址結構,而且沒辦法直接用程式一次載入所有文章來做排序、分頁或 RSS。散在路由資料夾裡的檔案,缺少了一層統一的內容模型(Content Model)。
Astro 的解法是 Content Collections:內容維持 Git 版控的純文字檔案,但在建置階段透過 Schema 進行型別校驗,並提供統一的 API 供頁面查詢與渲染。
src/pages,少了什麼?src/pages 適合單純的獨立頁面(如關於我們、隱私權政策)。若拿來放大量文章,會撞上三個限制:
date、那篇寫 pubDate,甚至漏填欄位,開發時都不會報錯,直到正式上線頁面渲染出空值或破版。(在 build 階段自動擋下欄位錯誤的做法見 Day 12。)Content Collections 將這些散落的內容檔抽離路由,宣告為帶有 Schema 的資料集合:內容路徑與網址脫鉤、欄位在 build 階段具備型別校驗,頁面也能透過統一 API 一次撈出整包資料。
建立一個 Collection 包含三個步驟:在設定檔宣告 Schema、在指定目錄放置內容檔、在頁面查詢並渲染。
src/content.config.ts)Astro v6 統一在 src/content.config.ts 定義內容集合(注意舊版 Astro 的路徑為 src/content/config.ts):
import { defineCollection, z } from 'astro:content';
import { glob } from 'astro/loaders';
const blog = defineCollection({
// loader 定義內容來源:glob 會載入指定目錄下的 .md 與 .mdx
loader: glob({ pattern: '**/[^_]*.{md,mdx}', base: './src/content/blog' }),
schema: z.object({
title: z.string(),
description: z.string(),
day: z.number().int().positive(), // 鐵人賽 Day 編號,方便排序
pubDate: z.coerce.date(), // 自動將字串轉為 Date 物件
tags: z.array(z.string()).default([]),
draft: z.boolean().default(false),
}),
});
export const collections = { blog };
這段設定包含兩個核心:
loader:指定內容來源。這裡使用內建的 glob loader 載入 src/content/blog 下的檔案。Day 11 會介紹如何載入 JSON 或外部 API 資料。schema:使用 Zod 定義 frontmatter 欄位規則。若某篇文章的 frontmatter 型別不符或缺少必填欄位,build 階段就會直接拋錯中斷。註:官方文件範例寫
import { z } from 'astro/zod',這裡使用import { z } from 'astro:content',兩者皆可(Astro 已 re-export Zod)。本文基準為 Astro v6。
文章存放在 src/content/blog/ 目錄下,開頭透過 frontmatter 填寫 Schema 規範的欄位:
---
title: 為什麼前端工程師要重新看 Astro
description: 從 server-first、content-first 與 islands 建立整個系列的技術主張。
day: 1
pubDate: 2026-06-03
tags: [astro, mental-model, islands]
---
正文從這裡開始……
在頁面中透過 getCollection 取得整組文章,再呼叫 render 將單篇文章轉換為 HTML 元件:
---
import { getCollection, render } from 'astro:content';
export const prerender = true; // 文章頁在 build 時先產生好靜態檔案
export async function getStaticPaths() {
const posts = await getCollection('blog', ({ data }) => !data.draft);
return posts.map((post) => ({
params: { slug: post.id }, // v6 統一使用 post.id
props: { post },
}));
}
const { post } = Astro.props;
const { Content } = await render(post); // v6 為 render(entry),非 entry.render()
---
<h1>{post.data.title}</h1>
<Content />
完成上述設定後,post.data 的所有欄位都享有 TypeScript 型別支援與編輯器自動補全。
在我們的實作專案中,首頁文章列表改由 getCollection('blog') 讀取並依 day 排序:

文章詳情頁則由動態路由 src/pages/blog/[...slug].astro 統一渲染,內容檔案的存放位置不再直接綁死 URL。
Schema 的價值在輸入錯誤時尤為明顯。若刻意在 Day 10 的 frontmatter 中漏填 description,執行 build 時會立即中斷並拋出明確錯誤:
[InvalidContentEntryDataError] blog → day-10-schema-error data does not match collection schema.
description: Required
錯誤在建置期就被攔截,避免缺漏欄位的頁面上線後造成破版或靜態生成異常。
Content-first 並非放諸四海皆準的方案,它與傳統資料庫驅動架構各有適用的場景:
| 面向 | WordPress(讀資料庫) | Astro(Markdown + Schema) |
|---|---|---|
| 內容存儲 | 資料庫(需維護 DB 與伺服器主機) | Git 版控中的 .md(零資料庫維護成本) |
| 內容修改 | 即時生效,無需重新部署 | 需重新 build 與部署(可由 CI/CD 自動化) |
| 編輯介面 | 內建視覺化後台(WYSIWYG) | 無內建後台(需另外整合 Headless CMS) |
| 版控與審查 | 內容存於資料庫,難以 diff 與版本回溯 | 支援 Git diff、Pull Request 與完整歷史回溯 |
| 產出形態 | 伺服器動態產生(常需快取外掛輔助) | 純靜態 HTML,直接部署至 Edge CDN |
| 資安面向 | 後台與外掛程式為長期潛在攻擊面 | 純靜態檔案,攻擊面極小 |
技術選型取決於網站維護模式:
關於圖片:純文字內容適合放在 Markdown 檔,圖片則需合適的儲存位置。Astro 內建圖片最佳化管線,少量文章插圖可直接放在原始碼目錄;若圖片量極大或來自外部上傳,實務上通常會搭配專屬圖床(詳見 Day 18)。
Content Collections 將散落在目錄中的內容檔案轉化為具備 Schema 約束、可被程式靈活查詢的資料集合。
Day 11 會介紹 Collection 的多樣化資料來源:除了本地 Markdown,還能透過不同的 Loader 載入 JSON 或外部 API 資料。Schema 的進階約束與錯誤防禦見 Day 12:Schema 與 Zod 驗證;文章、作者與分類的關聯處理見 Day 13:作者與分類關聯;資料量增加後的檢索與即時搜尋見 Day 14:搜尋與過濾 Island。
若專案確實需要資料庫以提供即時編輯介面,Day 28 會深入比較主流 CMS 與 Astro 的整合方案。
本日程式碼:step-10|只看這天的改動:step-09...step-10