iT邦幫忙

2025 iThome 鐵人賽

DAY 16
0
Modern Web

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

Astro(六):路由、文章列表的分頁及排序

  • 分享至 

  • xImage
  •  

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

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

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

靜態及動態路由(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,中的內容:

---
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.params 和 Astro.props 來取得 URL 的參數和內容。

排序 Sorting

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

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

新增 src/pages/blog/index.astro

---
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>

getStaticPaths() 中我們透過 posts.sort() 函式,將文章產出排序好的 Array。

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

分頁 Pagination

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

src/pages/blog/[page].astro

---
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, currentPagelastPage 和 url,用這些資訊就可以將分頁版本的文章列表 blog/[page].astro 所完成,以下是模板的部分:

<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.prev 和 url.next 加入「上一頁」、「下一頁」,以及 lastPage 最後一頁頁碼來顯示「共幾頁」。

重新導向進入頁面

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

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

astro.config.ts

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.ts 後就要記得將 blog/index.astro 給刪除。

文章總覽分頁示意

*文章總覽分頁示意

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

參考資料

  1. Astro - Static routes
  2. Astro - Dynamic routes
  3. Astro - Pagination
  4. Astro Routing - The page prop
  5. Astro TypeScript Configuration - Component Props

上一篇
Astro(五):如何製作文章列表?簡介 Content Collections
下一篇
Astro(七):巢狀分頁,如何製作標籤列表?
系列文
手刻部落格,從設計到部署的實戰攻略19
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言