Astro 架站筆記(四):文章列表的分頁,淺談路由、排序及分頁

當文章數量多到一定的程度(幾十篇、上百篇)時,在瀏覽文章列表時通常就要考慮分頁了。

這是因爲就如部落格這類內容導向的網站,人們決定是否要閱讀全文,除了標題可能不太足夠,因此當加上文章的摘要或前言後,要把數十篇「標題加摘要」的文章塞入一個列表內,會顯得資訊量太大。

除了對讀者的資訊量過大,將所有文章摘要塞入一個頁面,一次要下載的內容量也會過大,進而導致載入速度慢,甚至無法載入成功。因此,今天就來聊聊文章列表的排序及分頁功能!

靜態及動態路由(Static and Dynamic Routes)

在聊分頁之前,我們先看看 Astro 定義路由的方式:由於 Astro 採用 File-based Routing,因此我們在 src/pages 下的內容會自動對應到網站中的一個 URL,這稱作 Static routes,靜態路由。

相對於靜態路由,另外一種路由方式稱作 Dynamic routes,動態路由。我們在上一講中提到的 src/pages/blog/[id].astro 中的 [id] 就是設定 Dynamic routes 的方式,而被中括號所包起來的 id 就是 URL 中的參數。

使用的方式就類似上一篇談到建立 src/pages/blog/[id].astro,中的內容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
---
import { getCollection } from 'astro:content';
export const getStaticPaths = async () => {
return [
{ params: { id: '1' }, props: { content: 'Post content 1' } },
{ params: { id: '2' }, props: { content: 'Post content 2' } }
];
};

const { id } = Astro.params;
const { post } = Astro.props;
---

<h1>ID: {Astro.params.id}</h1>
<p>{Astro.props.content}</p>

Astro 會根據 getStaticPaths() 所回傳的 Array 來建立數個 URLs,以上述例子來說會建立兩個頁面 /blog/1/blog/2

而程式碼下方的 Astro 模板則可透過 Astro.paramsAstro.props 來取得 URL 的參數和內容。

排序

理解完了基本的靜態和動態路由,我們再來看看排序。

假設我們的部落格文章有 publishedTime 的資訊,格式如 2025-05-18 12:23:56,是個可以直接用 String 來比較時間先後順序的格式,那麼在取得一個 Collections 之後就能直接呼叫 JavaScript 原生的 sort() 函式,將最新的文章放在前面:

新增 src/pages/blog/index.astro

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
---
import { getCollection } from 'astro:content';

export const getStaticPaths = async () => {
const posts = await getCollection('blog');
const sortedPosts = posts.sort((a, b) => b.data.publishedTime.localeCompare(a.data.publishedTime));
return sortedPosts;
}
---
<ul>
{
sortedPosts((post) => (
<li>
<a href={`/blog/${post.id}`}>{post.data.title}</a>
<span> - {post.data.publishedTime}</span>
</li>
))
}
</ul>

如此一來,在文章總覽 /blog 中,就會按照新文章在最上面的排序來呈現。

分頁

再來看看這次的重點 Pagination,分頁的使用方式,在我們上面寫的 src/pages/blog/index.astro 中所顯示的文章列表僅有一個頁面而已,為了要達成分頁的目的,首先得將路由改成動態的方式,所以讓我們先將原本 blog/index.astro 中的內容調整後,建立以下檔案:

src/pages/blog/[page].astro

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

import type { Page } from 'astro';
import type { CollectionEntry } from 'astro:content';

export const getStaticPaths = async ({ paginate }) => { # 1
const posts = (await getCollection('blog')).sort((a, b) => b.data.publishedTime.localeCompare(a.data.publishedTime));
return paginate(posts, { pageSize: 3 }); # 2
};

# 3
type Props = {
page: Page<CollectionEntry<'blog'>>;
};
const { page } = Astro.props;
const { data: posts, currentPage, lastPage, url } = page;
---

從原本的一頁顯示所有文章,到使用動態路由產出能分頁的文章列表,有程式碼中標註的三點主要改動。

  1. getStaticPaths({ paginate }) 的第一個參數當中,取出 paginate() 這個函式

  2. 原本的回傳值由單純回傳文章的 Array 改成回傳一個 paginate(posts, { pageSize: 3 }) 呼叫後產生的物件,呼叫時需要傳原本的 posts 這個 Array,第二個參數稱作 options ,我們在此設定每頁的數量,為了測試方便我們挑了一個小數字 3,然而比較常見的數字會是 10 左右

  3. 最後,透過 Astro.props,我們可以取得每個頁面的資訊(Page Prop),包含 currentPage(當前頁碼)、lastPage(最後頁碼)、url(一個羅列不同 URL 的物件,底下包含 currentprevnextfirstlast)等等,以及其他先不在這邊贅述的資訊。

    這邊值得一提的是,若要將 Page 和我們前幾篇所定義的 blog Collection 綁定,在 Astro.props 的型別中使用,可以先用 type Props = { ... } 或 Interface 來定義 Component Props,在使用的時候 Astro 就會自動將此型別和 Astro.props 所連結。

在最後 frontmatter 的最後我們獲得的了 posts 的 Array, currentPage, lastPageurl,用這些資訊就可以將分頁版本的文章列表 blog/[page].astro 所完成,以下是模板的部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<html>
<head>
<meta charset="UTF-8" />
<title>部落格文章總覽 - 第 {currentPage} 頁</title>
</head>
<body>
<h1>部落格文章總覽</h1>
<ul>
{
posts.map((post) => (
<li>
<a href={`/blog/${post.id}`}>{post.data.title}</a>
<span> - {post.data.publishedTime}</span>
</li>
))
}
</ul>
<nav>
{currentPage > 1 && <a href={url.prev}>上一頁</a>}
<span>第 {currentPage} 頁,共 {lastPage} 頁</span>
{currentPage < lastPage && <a href={url.next}>下一頁</a>}
</nav>
</body>
</html>

<title> 部分,加上了現在所在的頁碼(currentPage),而導航的區塊則透過 url.prevurl.next 加入「上一頁」、「下一頁」,以及 lastPage 最後一頁頁碼來顯示「共幾頁」。

到目前為止,差一小步就完成分頁的開發了,當我們進入文章列表時,URL 的進入點通常是不含頁碼的,也就是 /blog 本身,但在有分頁的情況,我們預設應該要顯示第一頁(/blog/1)。

因此,只要在進入 /blog 這個 URL 的時候能夠自動導頁即可,可以在 Astro 設定檔中加入 redirects 的定義:

astro.config.cjs

1
2
3
4
5
6
7
import { defineConfig } from 'astro/config';
export default defineConfig({
/* ... */
redirects: {
'/blog': '/blog/1',
},
});

在重新導向時 Astro 會預設使用 HTTP Status Code 301 而非 302,是因為 301 表示永久導向(而 302 是暫時),這通常在 SEO 時會更有利一些。

另外,還記得我們有個 src/pages/blog/index.astro 的檔案嗎?目前 SSG 的模式下我們想要導頁的話需要用上述解法,但在 SSR(Server Side Rendering)的模式下可以將其改成 return Astro.redirect('/blog/1');,也能達到同樣的效果。

然而在 SSG 之下,修改 astro.config.cjs 後就要記得將 blog/index.astro 給刪除。

文章總覽結果

在測試上,由於我們 pageSize 設定為 3,只要在 src/content/blog/ 中加入四篇文章就能看看分頁的效果。