Astro 架站筆記(五):巢狀分頁,製作出標籤列表

上一講提到文章列表,能夠透過動態路由 .../[page].astro 搭配傳入 getStaticPaths({ paginate }) 中的 paginate() 函式來產生分頁,將所有的文章分成多頁。

但如果我們想製作能分頁的文章標籤(Tag)的列表呢?例如有 bluered 標籤,每個標籤下會有多篇文章,所以有類似 /tag/blue/1/tag/red/1/tag/red/2 這樣的 URLs,該如何使用 Astro 來開發呢?

這便會用到我們今天要聊聊的巢狀分頁。

新增文章 Tag

在建立巢狀分頁之前,我們先將文章的資訊加上 tags 這個屬性:

src/content.config.ts

1
2
3
4
5
6
7
const blog = defineCollection({
/* ... */
schema: z.object({
/* ... */
tags: z.array(z.string()),
}),
});

然後建立幾篇文章,這裡先以 Markdown 或 MDX 為例,在其 frontmatter 中加入 tags: [] 的資訊:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
---
# id: 1
layout: ../../layouts/BlogPostLayout.astro
title: '第一篇部落格貼文'
publishedTime: '2025-05-12 22:12:00'
updatedTime: '2025-05-12 22:12:00'
tags: ['a', 'b']
---

# Others

---

# id: 2

# ...

title: '第二篇部落格貼文'
tags: ['b', 'c']

---

---

# id: 3

# ...

title: '第三篇部落格貼文'
tags: ['c', 'd']

---

如此一來,就能在後續的處理中撈出有出現的 tags

巢狀分頁

接著便進入今天的主題:巢狀分頁(Nested Pagination)。這是一種進階的分頁功能,當我們檔案出現如 src/pages/tag/[tag]/[page].astro 的巢狀動態路由(包含 [tag] 及其底下的 [page]),就能做類似 /tag/blue/1/tag/red/1/tag/red/2 這樣針對不同 tag 的分頁,也因此稱之為巢狀分頁。

怎麼做呢?就繼續以部落格的標籤(tag)為例,來進行實作吧!

我們首先在 src/pages 下方建立 [tag] 資料,並在其之中建立 [page].astro 檔案。

取得所有 tags 及 tag 下的文章

就以上述新增的三篇部落格文章來說,這三篇文章分別對應到的 tags 是 a,bb,cc,d,而我們希望獲得的是「有哪些標籤」及「每個標籤下有哪些文章」,這樣才能為每個標籤建立頁面並列出對應文章。

所以最終的內容會長成:

1
2
3
4
5
6
const tagPostsMap = {
a: [{ id: 1 /* ... */ }],
b: [{ id: 1 /* ... */ }, { id: 2 /* ... */ }],
c: [{ id: 2 /* ... */ }, { id: 3 /* ... */ }],
d: [{ id: 3 /* ... */ }],
};

如果有了這個 tagPostsMap,就能夠透過 Object.keys(tagPostsMap) 來獲得 tags 陣列,也能知道 a, b, c, d 標籤下的文章。

以下是在 src/pages/tag/[tag]/[page].astro 所實作的 getTagPostsMap 程式碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { getCollection } from 'astro:content';
import type { CollectionEntry } from 'astro:content';

type TagPostsMap = { [tag: string]: CollectionEntry<'blog'>[] };

export const getTagPostsMap = (posts: CollectionEntry<'blog'>[]): TagPostsMap => {
const tagPostsMap: TagPostsMap = {};
for (const post of posts) {
for (const tag of post.data.tags) {
if (!tagPostsMap[tag]) tagPostsMap[tag] = [];
tagPostsMap[tag].push(post);
}
}
return tagPostsMap;
};

其實僅僅是將所有文章撈出來後做簡單處理,將每篇文章的 tag 丟到 Map 中當 key,其 value 是文章的陣列。

巢狀分頁的 getStaticPaths()

在上一講提到的簡單分頁情境中,getStaticPaths({ paginate }) 函式中,最後需要回傳一個 paginate() 呼叫後產生出的一組 Array of Object,Astro 會根據此 Array 產出多個頁面,將每個 Object 的頁碼塞入 Astro.params.page,實際在產出 URL 的 [page] 便會被 Astro.params.page 替代。

簡單分頁:部落格文章列表

而巢狀分頁會更複雜一些,我們首先從先前獲取的 tagPostsMap 出發,如下圖中最左邊的項目,這是一組 key-value pairs,key 就是 tag 名稱,而 value 則是文章(posts)的陣列。

巢狀分頁:標籤列表

我們需要對每個 tag 各自分頁,然後再將這些分頁的結果合併,最後由 Astro 產生靜態頁面。

例如下方的程式碼中,先對每個標籤(搭配其文章)呼叫 paginate(),透過 flatMap() 讓最終回傳值是一個 Array of Object,每個 Object 就是一個包含 tag + posts 的頁面。

1
2
3
4
5
6
7
8
9
10
export const getStaticPaths = async ({ paginate }) => {
const posts = await getCollection('blog');
const tagPostsMap = getTagPostsMap(posts);
return Object.entries(tagPostsMap).flatMap(([tag, posts]) =>
paginate(posts, {
pageSize: 1,
params: { tag },
})
);
};

這樣一來,動態路由 /[tag]/[page] 中就會根據 tagpaginate() 產出的 page 來填入對應的數值,最終得到 6 頁的結果。