Astro - 基礎入門2. Content Layer API

更新 發佈閱讀 16 分鐘

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 中是非常經典的步驟,請嘗試完成以下開發:

  1. 建立檔案 src/pages/posts/[id].astro。(注意檔名有中括號,代表這是動態路徑)。
  2. 使用 getStaticPaths() 函式來告訴 Astro 你有哪些文章要生成頁面。
  3. 使用 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. 規則對照表

raw-image

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 會進行以下的配對邏輯:

  1. 讀取檔名: 發現變數名稱叫做 id
  2. 呼叫函數: 執行 getStaticPaths() 拿到一個陣列。
  3. 比對 Key 值: 檢查陣列中每個物件的 params 部分。
  4. 驗證: 如果 params 裡面的 Key 剛好也叫 id,配對成功!
  5. 生成檔案: 如果配對成功,它就會根據 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 頁面。

留言
avatar-img
李昀瑾的沙龍
0會員
25內容數
李昀瑾的沙龍的其他內容
2026/01/08
Astro - 基礎入門1.第一課練習題實作
2026/01/08
Astro - 基礎入門1.第一課練習題實作
2026/01/08
第一課:Astro 的靈魂 —— 組件與佈局 (Layouts) 在 Astro 中,網頁是由一個個 .astro 檔案組成的。這類檔案最大的特色是:預設不發送任何 JavaScript 到瀏覽器,所以速度極快。
2026/01/08
第一課:Astro 的靈魂 —— 組件與佈局 (Layouts) 在 Astro 中,網頁是由一個個 .astro 檔案組成的。這類檔案最大的特色是:預設不發送任何 JavaScript 到瀏覽器,所以速度極快。
2026/01/07
Astro - 基礎入門 1 ~ 6 第一階段:環境初始化與核心觀念 第二階段:路由與佈局 (Layouts) 第三階段:核心進階 - Content Layer API (Astro 5 重點) 第四階段:孤島架構 (Islands) 與組件整合 第五階段:渲染模式與資料獲取 第六階段:這篇還沒有
2026/01/07
Astro - 基礎入門 1 ~ 6 第一階段:環境初始化與核心觀念 第二階段:路由與佈局 (Layouts) 第三階段:核心進階 - Content Layer API (Astro 5 重點) 第四階段:孤島架構 (Islands) 與組件整合 第五階段:渲染模式與資料獲取 第六階段:這篇還沒有
看更多