iT邦幫忙

2025 iThome 鐵人賽

DAY 17
0
Modern Web

手刻部落格,從設計到部署的實戰攻略系列 第 17

Astro(七):巢狀分頁,如何製作標籤列表?

  • 分享至 

  • xImage
  •  

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

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

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

新增文章 Tag

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

src/content.config.ts

const blog = defineCollection({
  /* ... */
  schema: z.object({
    /* ... */
    tags: z.array(z.string()),
  }),
});

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

---
# 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,而我們希望獲得的是「有哪些標籤」及「每個標籤下有哪些文章」,這樣才能為每個標籤建立頁面並列出對應文章。

所以最終的內容會長成:

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

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

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

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 的頁面。

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] 中就會根據 tag 及 paginate() 產出的 page 來填入對應的數值,最終得到 6 頁的結果。

參考資料

  1. Astro - Pagination
  2. Astro - Nested Pagination

上一篇
Astro(六):路由、文章列表的分頁及排序
下一篇
Astro(八):加入 Tailwind CSS,元件化的 CSS 利器
系列文
手刻部落格,從設計到部署的實戰攻略19
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言