在 Astro 部落格實作自動延伸閱讀
每次發布新文章,都要回頭修改舊文章的「延伸閱讀」連結?這不僅耗時,還容易遺漏。這個 Astro 專案會在靜態建置時依 tags 與 category 排序相關文章;當標籤不足以表達關係時,也能在 frontmatter 明確指定。
問題分析
在維護技術部落格時,常見的痛點包括:
- 延伸閱讀維護成本高:每篇新文章發布,相關舊文章都應該更新延伸閱讀
- 內部連結不一致:手動維護容易遺漏,導致 SEO 內部連結結構不完整
- 格式不統一:有些文章有延伸閱讀,有些沒有
解決方案架構
目前的流程在 Build 時先讀取文章清單,然後為每一篇文章建立相關文章清單:
getStaticPaths() 執行一次 │ ├── getSortedPosts() ─────────── 取得所有文章 │ └── getAllRelatedPostsMap() ──── 逐篇建立相關文章清單 │ └── Map<slug, PostForList[]> │ ▼每篇文章頁面 │ └── props.relatedPosts ───────── 直接使用預計算結果這樣做的好處不是把演算法變成 O(n),而是避免每個頁面生成流程重新讀取內容集合。生成出來的 HTML 也包含完整連結,爬蟲與不執行前端 JavaScript 的環境都能直接讀取。
實作步驟
步驟 1:相關文章計算函式
在 src/utils/content-utils.ts 新增計算邏輯:
/** * 計算單篇文章的相關文章 */function calculateRelatedPosts( currentSlug: string, currentTags: string[], currentCategory: string | undefined, allPosts: PostForList[], limit: number): PostForList[] { const otherPosts = allPosts.filter((post) => post.slug !== currentSlug);
const scoredPosts = otherPosts.map((post) => { let score = 0;
// 每個共同 tag 加 2 分 const postTags = post.data.tags || []; const commonTags = currentTags.filter((tag) => postTags.includes(tag)); score += commonTags.length * 2;
// 相同 category 加 1 分 if (currentCategory && post.data.category === currentCategory) { score += 1; }
return { post, score }; });
return scoredPosts .filter((item) => item.score > 0) .sort((a, b) => { if (b.score !== a.score) return b.score - a.score; return new Date(b.post.data.published).getTime() - new Date(a.post.data.published).getTime(); }) .slice(0, limit) .map((item) => item.post);}計分規則:
- 每個共同 tag:+2 分
- 相同 category:+1 分
- 分數相同時,較新的文章優先
步驟 2:在同一份文章清單上批次計算
在 getStaticPaths 中,內容清單只讀取一次,再交給每篇文章的相關性計算:
/** * 用同一份文章清單建立所有相關文章結果 */export async function getAllRelatedPostsMap( limit: number = 4): Promise<Map<string, PostForList[]>> { const allPosts = await getSortedPostsList(); const relatedMap = new Map<string, PostForList[]>();
for (const post of allPosts) { const related = calculateRelatedPosts( post.slug, post.data.tags || [], post.data.category || undefined, allPosts, limit ); relatedMap.set(post.slug, related); }
return relatedMap;}步驟 3:整合至文章頁面
在 src/pages/posts/[...slug].astro 的 getStaticPaths 中使用:
export async function getStaticPaths() { const blogEntries = await getSortedPosts(); // 不重複讀取內容集合 const relatedPostsMap = await getAllRelatedPostsMap(4);
return blogEntries.map((entry) => ({ params: { slug: entry.slug }, props: { entry, relatedPosts: relatedPostsMap.get(entry.slug) || [] }, }));}
const { entry, relatedPosts } = Astro.props;步驟 4:需要語意關係時用 frontmatter 覆寫
單靠共同標籤很適合一般推薦,卻不一定能表達「先讀這篇」或「這是後續排錯」的關係。目前 schema 也支援 related 陣列;有值時會優先使用它,找不到對應 slug 或指向自己時,建置會失敗,避免輸出壞連結。
related: - slug: cloudflare-workers-redirects-order-limits relation: background reason: 先理解靜態規則與動態路徑的排序方式步驟 5:建立 RelatedPosts 元件
建立 src/components/RelatedPosts.astro:
---import type { PostForList } from "@utils/content-utils";import { getPostUrlBySlug } from "@utils/url-utils";import { Icon } from "astro-icon/components";
interface Props { posts: PostForList[]; class?: string;}
const { posts, class: className } = Astro.props;---
{posts.length > 0 && ( <div class:list={["card-base rounded-xl p-6 mb-4", className]}> <h2 class="flex items-center gap-2 text-xl font-bold mb-4"> <Icon name="material-symbols:link-rounded" /> 延伸閱讀 </h2> <ul class="space-y-3"> {posts.map((post) => ( <li> <a href={getPostUrlBySlug(post.slug)}> {post.data.title} </a> </li> ))} </ul> </div>)}真正要觀察的效能邊界
每一篇文章仍要與其他文章比較共同 tags,因此這套目前實作在文章數量增加時仍屬兩兩比較,時間複雜度是 O(n²),不是 O(n)。先前把「只讀取一次文章集合」誤寫成整體 O(n);兩件事不相同。
對一般技術部落格來說,這個成本通常可以接受,但不應在沒有量測的情況下宣稱 Build 變快。當文章量明顯增加時,再用 tag 倒排索引或快取分數來降低比較次數;在此之前,直接量測 pnpm build 並保留結果更可靠。
常見問題
Q: 相關文章每次 Build 結果會不同嗎?
A: 相關性排序是確定性的(deterministic),相同的文章集合會產生相同的結果。但當你新增文章後,舊文章的相關推薦可能會更新,這是預期的行為,也是自動化的優勢——不需要手動維護。
Q: 什麼時候該用 related 明確指定?
A: 當兩篇文章的關係是前置知識、後續步驟、排錯或比較,而不是共同標籤時,就應該指定。這樣讀者看到的是有理由的連結,元件也會顯示對應的關係與說明。
Q: 如何自訂相關性計分規則?
A: 修改 calculateRelatedPosts 函式中的計分邏輯。例如,你可以增加「相同作者 +3 分」、「發布日期相近 +1 分」等規則。目前的規則是:共同 tag +2 分、相同 category +1 分。
Q: 這個方案適用於其他 SSG 框架嗎?
A: 概念相同,實作細節不同。Next.js 可以在 getStaticProps 中做類似的事;Hugo 可以用 .Site.RegularPages 搭配 intersect 函式;Jekyll 需要用 Liquid 模板或外掛。核心思路都是在 Build 階段預計算。
這個做法的實際價值是:把文章清單讀取集中在建置期、讓一般文章依 tags 與 category 自動推薦,並在關係必須精準時允許作者明確指定。不要把批次處理誤當成 O(n) 演算法;用目前網站的建置時間驗證,才知道是否真的需要再優化。
參考資料:
回報錯字、失效連結,或告訴我你想看的延伸主題。