EP.09網頁開發實戰

部落格導航與搜尋系統
從結構設計到演算法實作

如何建立一個可擴充的配置系統?如何實作高效的即時搜尋與滾動追蹤目錄? 本篇將深度拆解 chullin.vercel.app 的導航架構。

Joseph Chen 2026 12 min read React · IntersectionObserver · Filter Search

「當文章超過 50 篇時,傳統的上下篇導航已經不夠用了。」

隨著 LeetCode 系列與 AI 專題的增加,使用者需要更直覺的方式在不同類別間跳轉。 我決定實作一個「全站導覽抽屜」與「動態目錄系統」。這不僅是 UI 的改變, 更是從硬編碼(Hardcoded)轉向配置驅動(Configuration-driven)的重大重構。

核心架構:單一數據源

為了避免每次新增文章都要修改多個地方,我建立了一個中心化的配置檔 config/blog.tsx。 所有的導航組件、搜尋引擎、甚至是首頁的文章清單,都從這個檔案讀取數據。

config/blog.tsx
export type Post = {
  title: string;
  subtitle: string;
  href: string;
  ep?: string;
  // ... 其他元數據
};

export type Series = {
  id: string;
  label: string;
  posts: Post[];
  // ... 類別資訊
};

export const series: Series[] = [
  { id: 'leetcode', label: 'LeetCode 系列', posts: [...] },
  { id: 'ai', label: 'AI 離線部署', posts: [...] },
];

💡 優點:當我移動檔案目錄(如將文章歸類到 leetcode 資料夾)時,只需更新這裡的 href,全站導航就會自動同步。

實戰一:全站即時搜尋

懸浮導覽列中的搜尋框使用了雙層過濾演算法。它不僅過濾文章標題, 還會根據類別名稱進行匹配。

搜尋邏輯虛擬碼

  1. 使用者輸入關鍵字 (Query)
  2. 過濾 Series:
    • 若 Series 標籤符合 Query → 保留該系列所有文章
    • 若 Series 標籤不符 → 檢查該系列下的每一篇文章
  3. 過濾 Post:檢查標題或副標題是否包含 Query
  4. 重新封裝:只顯示有匹配結果的類別
FloatingNav.tsx (Search Logic)
const filteredSeries = series.map(s => {
  // 1. 如果類別名稱本身就匹配,直接回傳整個類別
  if (s.label.toLowerCase().includes(query.toLowerCase())) return s;

  // 2. 否則過濾底下的文章
  const filteredPosts = s.posts.filter(p => 
    p.title.toLowerCase().includes(query.toLowerCase()) ||
    p.subtitle.toLowerCase().includes(query.toLowerCase())
  );

  return { ...s, posts: filteredPosts };
}).filter(s => s.posts.length > 0); // 3. 只留下有文章的類別
🛠 Tech: React useMemo + Filter⚙️ Complexity: O(S * P) - S=類別數, P=文章數

實戰二:動態目錄與滾動追蹤

目錄 (Table of Contents) 必須具備兩個核心功能:自動提取標題滾動高亮

自動提取

使用 document.querySelectorAll('h2, h3')。 為了避免抓到頁首或頁尾,我們將範圍限制在 article 標籤內。

滾動追蹤

放棄監聽 scroll 事件(效能差),改用 IntersectionObserver。 當標題進入視窗頂部 20% 範圍時,觸發高亮。

TOC.tsx (Intersection Observer)
useEffect(() => {
  const observer = new IntersectionObserver(
    (entries) => {
      // 找出目前正在視窗中且最靠近頂部的標題
      const visible = entries.find(e => e.isIntersecting);
      if (visible) setActiveId(visible.target.id);
    },
    { rootMargin: '-100px 0px -80% 0px' } // 觸發區間設定在視窗上方
  );

  headings.forEach(h => {
    const el = document.getElementById(h.id);
    if (el) observer.observe(el);
  });

  return () => observer.disconnect();
}, [headings]);

效能與交互體驗優化

  • 佈局抖動防止

    導覽列抽屜開啟時,會導致頁面滾動條消失,產生佈局跳動。 我使用了 HeroUIDrawer 組件,它會自動處理 overflow: hidden 與 padding 補償。

  • 🎭

    流暢過場動畫

    使用 framer-motionAnimatePresence。 當使用者在導覽列中切換類別時,文章清單會以微小的位移與淡入效果呈現,減少視覺疲勞。

本篇重點回顧

📋配置驅動設計:將所有文章元數據抽離至 config.tsx,達成 Single Source of Truth。
🔍層次化搜尋:先匹配類別,再匹配文章,提升搜尋的相關性與效率。
👁️IntersectionObserver:高效實作目錄高亮,避免 scroll 事件導致的 CPU 負載。
📐響應式導航:寬螢幕使用 TOC,所有設備支援 Floating Sidebar,確保一致的跳轉體驗。
Next.js
React
Navigation
Search Algorithm
IntersectionObserver
Performance
EP.09