1081 字
5 分鐘
在 Astro 部落格實作自動延伸閱讀

每次發布新文章,都要回頭修改舊文章的「延伸閱讀」連結?這不僅耗時,還容易遺漏。本文記錄如何在 Astro 部落格實作自動化的相關文章推薦系統。

問題分析#

在維護技術部落格時,常見的痛點包括:

  1. 延伸閱讀維護成本高:每篇新文章發布,相關舊文章都應該更新延伸閱讀
  2. 內部連結不一致:手動維護容易遺漏,導致 SEO 內部連結結構不完整
  3. 格式不統一:有些文章有延伸閱讀,有些沒有

解決方案架構#

我們的目標是在 Build 時自動計算相關文章:

getStaticPaths() 執行一次
├── getSortedPosts() ─────────── 取得所有文章
└── getAllRelatedPostsMap() ──── 一次迴圈計算所有相關文章
└── Map<slug, PostForList[]>
每篇文章頁面
└── props.relatedPosts ───────── 直接使用預計算結果

為什麼選擇 Build 時生成而非前端動態載入?因為 SEO。Build 時生成的 HTML 包含完整連結,搜尋引擎爬蟲可以直接讀取。

實作步驟#

步驟 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 只計算一次:

/**
* 一次性計算所有文章的相關文章
* 時間複雜度:O(n) 而非 O(n²)
*/
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].astrogetStaticPaths 中使用:

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;

建立 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>
)}

效能比較#

文章數量優化前 (O(n²))優化後 (O(n))
76 篇5,776 次計算76 次計算
200 篇40,000 次200 次
500 篇250,000 次500 次

實測 Build 時間:從 6.88s 降到 5.10s(減少 26%)

常見問題#

Q: 相關文章每次 Build 結果會不同嗎?#

A: 相關性排序是確定性的(deterministic),相同的文章集合會產生相同的結果。但當你新增文章後,舊文章的相關推薦可能會更新,這是預期的行為,也是自動化的優勢——不需要手動維護。

Q: 如何自訂相關性計分規則?#

A: 修改 calculateRelatedPosts 函式中的計分邏輯。例如,你可以增加「相同作者 +3 分」、「發布日期相近 +1 分」等規則。目前的規則是:共同 tag +2 分、相同 category +1 分。

Q: 這個方案適用於其他 SSG 框架嗎?#

A: 概念相同,實作細節不同。Next.js 可以在 getStaticProps 中做類似的事;Hugo 可以用 .Site.RegularPages 搭配 intersect 函式;Jekyll 需要用 Liquid 模板或外掛。核心思路都是在 Build 階段預計算。

總結#

  1. 自動化:根據 tags 和 category 自動推薦相關文章,不再手動維護
  2. 效能優化:使用 Map 預計算,時間複雜度從 O(n²) 降到 O(n)
  3. SEO 友善:Build 時靜態生成,搜尋引擎可完整抓取內部連結

測試環境:macOS Sequoia 15.3, Astro 5.12.8, 2026/02/01

參考來源:

Astro Content Collections

在 Astro 部落格實作自動延伸閱讀
https://laplusda.com/posts/astro-auto-related-posts/
作者
Zero
發佈於
2026-02-01
許可協議
CC BY-NC-SA 4.0
這篇文章有幫助嗎?

回報錯字、失效連結,或告訴我你想看的延伸主題。