Astro 5.1.x 的重點在於如何管理內容
以前我們可能要手動 fs.readFileSync,現在我們用 Content Layer API,它能讓你的 Markdown 檔案擁有強大的型別檢查 (Type Safety)。
第二課:Content Collections (內容集) —— 處理 Markdown 與資料
在 Astro 中,當你需要寫部落格文章、產品列表或文件時,我們會把這些內容放在 src/content/ 資料夾中。
1_核心概念:Schema (架構)
你會定義一個「規範」(例如:每篇文章必須有標題、日期、標籤)。
如果某篇文章漏寫了標題,Astro 在編譯時就會報錯提醒你,這就是紮實的開發流程。
2_手把手實作:設定你的第一組內容
第一步:定義架構 請建立(或修改)檔案 src/content/config.ts:
import { defineCollection, z } from 'astro:content'
import { glob } from 'astro/loaders' // Astro 5 的新寫法
const blog = defineCollection({
// 使用 glob loader 載入 src/content/blog/ 下的所有 md 檔案
loader: glob({ pattern: '**/[^_]*.md', base: './src/content/blog' }),
schema: z.object({
title: z.string(),
pubDate: z.date(),
description: z.string(),
author: z.string(),
tags: z.array(z.string()),
}),
})
export const collections = { blog }
第二步:撰寫內容 建立資料夾 src/content/blog/,並在裡面建立一篇文章 post-1.md:
---
title: '我的第一篇 Astro 文章'
pubDate: 2024-01-20
description: '這是關於我學習 Astro 5.0 的心得。'
author: 'Astro 學習者'
tags: ['astro', 'learning']
---
# 歡迎來到我的部落格
這是我用 **Astro** 寫的第一篇文章。
這裡可以使用 Markdown 的語法,非常方便!
第三步:在頁面顯示列表 修改或建立 src/pages/blog.astro:
---
import { getCollection } from 'astro:content'
import type { CollectionEntry } from 'astro:content'
import BaseLayout from '../layouts/BaseLayout.astro'
const pageTitle = '我的部落格'
const allPosts: CollectionEntry<'blog'>[] = await getCollection('blog')
---
<BaseLayout pageTitle={pageTitle}>
<ul>
{
allPosts.map((post) => (
<li>
<a href={`/posts/${post.id}`}>{post.data.title}</a>
<span> - {post.data.pubDate.toLocaleDateString()} </span>
</li>
))
}
</ul>
</BaseLayout>
第四步:修改 src/layouts/BaseLayout.astro:
BaseLayout 的 nav 增加 blog 連結
...(省略)
<nav>
<a href="/">首頁</a>
<a href="/about">關於我</a>
<a href="/blog">部落格</a> <!-- 增加這個 -->
</nav>
...(省略)
📝 第二課練習題
任務目標: 實作「動態路由」,讓點擊列表後能看到文章內容。
這在 Astro 中是非常經典的步驟,請嘗試完成以下開發:
- 建立檔案
src/pages/posts/[id].astro。(注意檔名有中括號,代表這是動態路徑)。 - 使用
getStaticPaths()函式來告訴 Astro 你有哪些文章要生成頁面。 - 使用
render(post)函式(從astro:content匯入)來渲染 Markdown 內容。
參考提示代碼:
---
import { getCollection, render } from 'astro:content';
import BaseLayout from '../../layouts/BaseLayout.astro';
export async function getStaticPaths() {
const posts = await getCollection('blog');
return posts.map(post => ({
params: { id: post.id },
props: { post },
}));
}
const { post } = Astro.props;
const { Content } = await render(post);
---
<BaseLayout pageTitle={post.data.title}>
<Content />
</BaseLayout>
挑戰題: 在 src/pages/blog.astro 頁面中,試著讓列表按日期排序,最新的文章排在最前面。(提示:使用 JavaScript 的 sort() 方法操作 allPosts)。
第二課練習題實作放在下一篇 ( 我也是第一次學習,自己試作,不一定是對的喔👍 )
📚 額外筆記
⚠️ ① allPosts 可能是 [] 空陣列
這是一個在 Astro 5.0 中常見的配置問題。
雖然程式碼看起來大致正確,但 allPosts 回傳空陣列 []
通常是因為 Content Layer (Astro 5.0) 的載入路徑或內容讀取邏輯出現了細微的偏差。
以下是幾個最可能的排查方向與修正建議:
1. 檢查檔案路徑
(最常見原因,但我不是遇到這個,是第4個 : 需要重新 npm run dev)
在您的 config.ts 中,base 設定為 ./src/content/blog:
loader: glob({ pattern: '**/[^_]*.md', base: "./src/content/blog" }),
如果您的檔案結構是 src/content/blog/post-1.md,這意味著 glob 會在 blog 資料夾內尋找。
- 問題: 如果您的路徑是相對路徑,有時候因為執行環境不同會抓不到。
- 修正: 嘗試將
base改為更明確的絕對路徑形式(相對於專案根目錄):base:"src/content/blog"(去掉前面的./)。
2. 模式匹配問題
您的 pattern 是 '**/[^_]*.md'。
- 這個模式會遞迴尋找所有子資料夾,並排除掉以底線
_開頭的檔案。 - 請確認您的
post-1.md確實存放在src/content/blog/底下,而不是直接放在src/content/。
3. 日期格式與 Zod 解析
在您的 Markdown YAML 中:
pubDate: 2024-01-20
在 Astro 中,YAML 的日期有時會被解析為字串。雖然 Zod 的 z.date() 會嘗試轉換,但如果轉換失敗,該筆資料可能會被過濾掉或報錯。
- 檢查點: 啟動 Dev server 時,終端機(Terminal)是否有顯示 Content Collection Error?
如果有 Schema 驗證錯誤,getCollection就不會回傳該資料。
4. 快取緩存問題
Astro 5.0 的 Content Layer 依賴於 .astro 資料夾中的快取。
- 解決方案: 嘗試刪除專案根目錄下的
.astro資料夾,然後重新啟動開發伺服器:
rm -rf .astro # 這個應該不需要
npm run dev
⚠️ ② 路由參數名稱不匹配
當我故意修改 src/pages/posts/[id].astro 的 getStaticPaths id 改成 iid
export const getStaticPaths = async () => {
const posts: CollectionEntry<'blog'>[] = await getCollection('blog')
return posts.map((post) => ({
params: { iid: post.id }, // 原本是 { id: post.id }
props: { post },
}))
}
如果您在 src/pages/posts/[id].astro(或類似路徑)中使用這段程式碼,主要會遇到兩個問題:
一個是參數名稱不匹配導致的 404 錯誤,另一個是 TypeScript 型別定義的潛在問題。
1. 路由參數名稱不匹配 (iid vs id)
- 您的代碼:
params: { iid: post.id } - 檔案名稱:
如果您的檔案路徑是src/pages/posts/[id].astro, 那麼 Astro 會尋找params.id而不是params.iid。 - 結果:
當您訪問/posts/post-1時,Astro 會找不到對應的頁面, 因為它期待的 key 是id。
2. TypeScript 與 CollectionEntry 的引用
在 Astro 5.0 中,CollectionEntry 的引用方式建議更為明確。雖然您的寫法在大多數情況下可行,但如果沒有正確匯入 CollectionEntry,會導致編譯錯誤。
( 這一段是不是看不懂? 沒關係我也看不懂, 所以我有往下繼續追問👍 )
🟡 Astro 約定優於配置(Convention over Configuration)的規則
簡單來說:你在檔案名稱括號 [] 裡面寫什麼,你的 params 就要叫什麼。
我們可以把檔案路徑看成一個「變數宣告」:
1. 規則對照表

2. 為什麼會報錯?
如果你把檔案命名為 [id].astro,但在 getStaticPaths 裡回傳 iid,這就像是你宣告了一個叫 id 的變數,卻試圖餵給它一個叫 iid 的資料,Astro 會對不起來。
🟢 背後原理
這背後的原理涉及到 Astro 的 動態路由(Dynamic Routing) 機制與 靜態生成(SSG) 的邏輯。
我們可以從「路由變數化」和「編譯流程」這兩個核心概念來拆解:
1. 路由的變數宣告(檔名即變數)
在 Astro 中,檔案名稱中的 [ ] 括號不是裝飾,它在編譯器眼中代表一個變數佔位符。
- 當你建立
src/pages/posts/[id].astro時,你等於在告訴 Astro:
「請幫我在/posts/路徑下建立一系列頁面,而這些頁面的名稱會根據一個叫做id的變數來決定。」
這就像在程式語言中定義一個函數: function generatePage(id) { ... }
2. getStaticPaths 的數據填充
當 Astro 在編譯(Build)你的網站時,它會執行 getStaticPaths()。這個函數的唯一任務就是:提供數據來填滿你定義的變數。
Astro 會進行以下的配對邏輯:
- 讀取檔名: 發現變數名稱叫做
id。 - 呼叫函數: 執行
getStaticPaths()拿到一個陣列。 - 比對 Key 值: 檢查陣列中每個物件的
params部分。 - 驗證: 如果 params 裡面的 Key 剛好也叫 id,配對成功!
- 生成檔案: 如果配對成功,它就會根據
post.id的值(例如 post-1),產生實體的 HTML 檔案dist/posts/post-1/index.html。
3. 為什麼寫 iid 會報錯?
如果你在檔案名稱寫 [id],但在 params 傳回 { iid: post.id },對 Astro 來說就像:
- 需求端: 我要一個叫
id的資料。 - 供應端: 我這有一份叫
iid的資料。 - 結果: 屬性不匹配(Property Mismatch)。
Astro 無法從你的數據中找到名為id的值,因此它不知道該把這個頁面命名為什麼。
4. 編譯時的解構(Destructuring)
在 Astro 內部處理時,其實可以想像成它對你回傳的物件進行了解構賦值:
// 假設檔名是 [id].astro
const { id } = params // 如果你回傳 { iid: 'post-1' },這裡的 id 會是 undefined
因為 id 變成了 undefined,Astro 就無法為這個路徑生成有效的 URL。
📌 總結原理
Astro 的動態路由是一種對等映射(Mapping):
- 檔案系統 定義了變數的 名稱 (Key)。
getStaticPaths定義了變數的 數值 (Value)。- 兩者的 Key 必須完全一致,映射才能成立。
這套機制讓你只需要寫一個 .astro 模板,就能根據資料庫或 Markdown 檔案的數量,自動批次生成成千上萬個獨立的 HTML 頁面。