Astro 實戰:為靜態部落格打造標籤系統與即時搜尋功能
本篇文章詳細解說如何在 Astro 部落格中,透過 getStaticPaths 實作靜態標籤路由,並結合 Fuse.js 打造高效的前端模糊搜尋功能。
前言
隨著靜態網站的內容庫日益增長,如何提升內容的「可發現性 (Discoverability)」變得至關重要。在這篇文章中,我將記錄如何在我的 Personal Digital HQ 專案中實作兩個關鍵功能:
- 標籤系統 (Tag System):一個靜態分類系統,為每個標籤生成專屬的彙整頁面。
- 全文搜尋 (Full-Text Search):使用 Fuse.js 實作的客戶端搜尋引擎。
第一部分:標籤系統 (Tag System)
我們的目標是對文章進行分類,並允許使用者瀏覽特定主題下的所有文章。由於 Astro 是一個靜態網站產生器 (SSG),我們可以利用這個特性,在建置時期 (Build Time) 掃描所有 Markdown 內容,為每一個獨一無二的標籤自動生成專屬頁面。
1. 資料結構設計
我們的內容集合 (Content Collection) 已經在 src/content/config.ts 定義好了。其中 tags 欄位是一個字串陣列:
// src/content/config.ts
schema: z.object({
// ... 其他欄位
tags: z.array(z.string()).default([]),
})
2. 建立標籤頁面路由
我們使用 Astro 的動態路由功能 [tag].astro 來生成頁面。核心關鍵在於 getStaticPaths 函式,它必須回傳所有部落格文章中出現過的標籤清單。
建立檔案 src/pages/blog/tag/[tag].astro:
---
// src/pages/blog/tag/[tag].astro
import BaseLayout from '../../../layouts/BaseLayout.astro';
import { getCollection } from 'astro:content';
export async function getStaticPaths() {
const allPosts = await getCollection('blog', ({ data }) => !data.draft);
// 提取所有不重複的標籤
const uniqueTags = [...new Set(allPosts.map((post) => post.data.tags).flat())];
return uniqueTags.map((tag) => {
// 篩選出包含此標籤的文章
const filteredPosts = allPosts.filter((post) =>
post.data.tags.includes(tag)
).sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf());
return {
params: { tag },
props: { posts: filteredPosts },
};
});
}
const { tag } = Astro.params;
const { posts } = Astro.props;
---
<!-- 這裡放置 Template 渲染程式碼 -->
這個函式主要做了兩件事:
uniqueTags:掃描所有文章,找出每一個獨一無二的標籤。return:為每個標籤生成對應的路由,並將相關文章作為props傳遞給頁面。
3. 資料視覺化:標籤雲 (Tag Cloud)
為了讓標籤更容易被存取,我們建立了一個可重用的 TagCloud 元件,它會計算每個標籤出現的頻率,並依熱門程度排序。
---
// src/components/blog/TagCloud.astro
import { getCollection } from 'astro:content';
const allPosts = await getCollection('blog', ({ data }) => !data.draft);
const tags = allPosts
.flatMap((post) => post.data.tags)
.reduce((acc, tag) => {
acc[tag] = (acc[tag] || 0) + 1;
return acc;
}, {} as Record<string, number>);
// 依數量排序
const sortedTags = Object.entries(tags).sort((a, b) => b[1] - a[1]);
// 智慧摺疊邏輯 (Smart Collapse)
const VISIBLE_COUNT = 12;
const topTags = sortedTags.slice(0, VISIBLE_COUNT);
const hiddenTags = sortedTags.slice(VISIBLE_COUNT);
---
<div class="glass p-6 rounded-xl border border-hacker-border/30">
<h3 class="text-xl font-bold text-hacker-text mb-4"># Topics</h3>
<div class="flex flex-wrap gap-2">
{/* 顯示熱門標籤與展開按鈕... */}
</div>
</div>
我們隨後將此元件整合到部落格的側邊欄,在桌機版本上實作了兩欄式佈局 (8欄內容、4欄側邊欄)。
第二部分:使用 Fuse.js 實作客戶端搜尋
對於這種規模的靜態部落格來說,客戶端 (Client-Side) 的模糊搜尋既高效又易於實作。我們選擇了 Fuse.js,因為它輕量且具備強大的模糊匹配能力。
1. 生成搜尋索引 (Search Index)
由於沒有後端資料庫可供執行時 (Runtime) 查詢,我們需要在建置時期生成一份包含所有文章資料的 JSON 索引。Astro 的 API 端點功能 (pages 目錄下的 .ts 檔案) 非常適合用來做這件事。
建立 src/pages/api/search.json.ts:
import { getCollection } from 'astro:content';
export async function GET() {
const posts = await getCollection('blog', ({ data }) => !data.draft);
// 映射出輕量的搜尋索引物件
const searchIndex = posts.map(post => ({
title: post.data.title,
description: post.data.description,
tags: post.data.tags,
slug: post.slug,
pubDate: post.data.pubDate,
}));
return new Response(JSON.stringify(searchIndex), {
headers: { 'Content-Type': 'application/json' }
});
}
這會產生一個 /api/search.json 端點,我們的前端程式碼可以在載入時讀取它。
2. 搜尋元件 (Search Component)
我們建立了一個 React 元件 Search.tsx,它負責:
- 在元件掛載 (Mount) 時抓取 JSON 索引。
- 初始化 Fuse 實例。
- 提供輸入框並顯示即時搜尋結果。
// Fuse.js 關鍵設定
const fuseInstance = new Fuse(data, {
keys: [
{ name: 'title', weight: 0.7 }, // 標題權重最高
{ name: 'tags', weight: 0.2 }, // 標籤次之
{ name: 'description', weight: 0.1 } // 描述最低
],
threshold: 0.3, // 模糊匹配門檻 (數值越低越精確)
includeMatches: true
});
3. 整合
最後,我們將 Tag Cloud 和 Search Widget 整合到 src/pages/blog/index.astro 的全新側邊欄佈局中。這將原本單純的列表式部落格,轉變為一個具備完整導航功能的知識庫。
結論
透過這些功能的加入,部落格的使用者體驗 (UX) 得到了顯著提升。靜態生成的標籤頁面確保了優秀的 SEO 表現,而客戶端搜尋則為使用者提供了即時的反饋。最重要的是,這一切都在沒有複雜後端架構的情況下完成,完美保留了靜態網站架構的效能優勢。