Skip to content
Anonymous

Astro 實戰:為靜態部落格打造標籤系統與即時搜尋功能

本篇文章詳細解說如何在 Astro 部落格中,透過 getStaticPaths 實作靜態標籤路由,並結合 Fuse.js 打造高效的前端模糊搜尋功能。

#astro #web-development #tutorial #search #ssg

前言

隨著靜態網站的內容庫日益增長,如何提升內容的「可發現性 (Discoverability)」變得至關重要。在這篇文章中,我將記錄如何在我的 Personal Digital HQ 專案中實作兩個關鍵功能:

  1. 標籤系統 (Tag System):一個靜態分類系統,為每個標籤生成專屬的彙整頁面。
  2. 全文搜尋 (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 渲染程式碼 -->

這個函式主要做了兩件事:

  1. uniqueTags:掃描所有文章,找出每一個獨一無二的標籤。
  2. 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,它負責:

  1. 在元件掛載 (Mount) 時抓取 JSON 索引。
  2. 初始化 Fuse 實例。
  3. 提供輸入框並顯示即時搜尋結果。
// 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 CloudSearch Widget 整合到 src/pages/blog/index.astro 的全新側邊欄佈局中。這將原本單純的列表式部落格,轉變為一個具備完整導航功能的知識庫。


結論

透過這些功能的加入,部落格的使用者體驗 (UX) 得到了顯著提升。靜態生成的標籤頁面確保了優秀的 SEO 表現,而客戶端搜尋則為使用者提供了即時的反饋。最重要的是,這一切都在沒有複雜後端架構的情況下完成,完美保留了靜態網站架構的效能優勢。