跳至正文

朋友问我最近博客怎么不更新了,我说别急,在憋个大的。其实是给博客加了个新功能:**文章内链推荐系统**。折腾了3天,踩了11个坑,差点把数据库干崩了。

先说说为什么搞这个。我博客用的是 Hugo 静态站,文章多了之后,读者看完一篇就跑了,跳出率快80%。我就想加个“你可能还想看”的模块,根据当前文章标签,自动推荐相关文章。听起来简单对吧?结果一动手全是雷。

**第一个坑:Hugo 的 .Site.RegularPages 性能问题**

我第一版代码写得很粗暴,直接在 single.html 模板里遍历所有文章算标签相似度:

“`go
{{ $related := slice }}
{{ range .Site.RegularPages }}
{{ if ne .Permalink $.Permalink }}
{{ $intersect := intersect $.Params.tags .Params.tags }}
{{ if ge (len $intersect) 1 }}
{{ $related = $related | append . }}
{{ end }}
{{ end }}
{{ end }}
“`

本地跑没问题,一部署到 Vercel,构建时间从30秒飙到4分17秒。我博客现在有400多篇文章,每次构建要遍历所有页面算交集,复杂度 O(n^2)。更坑的是,Hugo 的 `intersect` 函数在小集合上还行,但每篇文章标签平均5个,400篇文章就是2000次集合运算。

**解决方案:预计算 + 缓存**

我把推荐逻辑从模板层移到了构建前的脚本里,用 Python 写了个预处理程序,在 Hugo 构建之前跑:

“`python
import yaml, os, json
from collections import defaultdict

posts = []
for root, dirs, files in os.walk(‘content/posts’):
for f in files:
if f.endswith(‘.md’):
with open(os.path.join(root, f), ‘r’) as fp:
content = fp.read()
# 解析 frontmatter
parts = content.split(‘—‘)
if len(parts) >= 3:
meta = yaml.safe_load(parts[1])
posts.append({
‘path’: f.replace(‘.md’, ”),
‘title’: meta.get(‘title’, ”),
‘tags’: meta.get(‘tags’, [])
})

# 构建标签倒排索引
tag_index = defaultdict(list)
for i, post in enumerate(posts):
for tag in post[‘tags’]:
tag_index[tag].append(i)

# 计算每篇文章的推荐列表
recommendations = {}
for i, post in enumerate(posts):
scores = defaultdict(int)
for tag in post[‘tags’]:
for j in tag_index[tag]:
if i != j:
scores[j] += 1
# 取前5篇
top = sorted(scores.items(), key=lambda x: -x[1])[:5]
recommendations[post[‘path’]] = [posts[idx][‘path’] for idx, _ in top]

# 输出为 JSON
with open(‘data/recommendations.json’, ‘w’) as f:
json.dump(recommendations, f)
“`

然后在 Hugo 模板里直接读这个 JSON,复杂度降到 O(1):

“`go
{{ $recs := getJSON “/data/recommendations.json” }}
{{ $related := index $recs .File.Path }}
{{ range first 5 $related }}
{{ .Title }}
{{ end }}
“`

构建时间降回35秒,完美。

**第二个坑:标签相似度太弱智**

纯标签交集推荐有个致命问题:如果一篇文章标签是“Go, 并发, 性能”,另一篇是“Go, 并发, 调试”,交集是2,但内容可能完全不相关。我有一篇讲“Go 协程池设计”的文章,推荐列表里全是“Go 环境搭建”这种入门文,读者点进去一脸懵逼。

**解决方案:TF-IDF 加权**

我把标题和摘要也纳入相似度计算,用 TF-IDF 给词加权。标签权重设为3,标题词权重2,摘要词权重1。用 Python 的 scikit-learn 算余弦相似度:

“`python
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity

documents = []
for post in posts:
text = ‘ ‘.join(post[‘tags’] * 3 + post[‘title’].split() * 2 + post[‘summary’].split())
documents.append(text)

vectorizer = TfidfVectorizer()
tfidf_matrix = vectorizer.fit_transform(documents)
similarities = cosine_similarity(tfidf_matrix)
“`

这样推荐出来的文章,语义相关性高了不止一个档次。比如“Go 协程池”那篇,现在推荐的是“Go 工作池模式实战”和“Goroutine 泄漏排查指南”,终于像人话推荐的了。

**第三个坑:推荐列表全是自己**

调试的时候发现,有些文章推荐列表里居然包含自己。查了半天,是因为我文件名处理有 bug——`strings.Replace` 把路径里的 `/posts/` 替换成空的时候,把 `posts` 目录下所有文件都匹配了。后来改成用 `strings.TrimPrefix` 才解决。

**第四个坑:移动端排版爆炸**

推荐模块在 PC 上显示正常,手机上一看,5篇文章挤成一坨,点都点不到。加了 CSS 限制:

“`css
.related-posts {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.related-posts a {
flex: 1 1 45%;
padding: 8px;
font-size: 14px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
“`

**第五个坑:新文章没有推荐**

刚发布的第一篇文章,因为没有历史数据,推荐列表是空的。我在预处理脚本里加了兜底逻辑:如果推荐少于3篇,就随机取最近发布的5篇文章作为补充。

**第六个坑:标签大小写不统一**

有的文章标签写的是 “Go”,有的是 “go”,有的是 “Golang”。预处理的时候全转小写才搞定。

**第七个坑:性能瓶颈在 YAML 解析**

400多篇文章,Python 的 yaml.safe_load 跑了快8秒。改成用 ruamel.yaml 的快速模式,降到3秒。但还是慢,最终决定用 Go 重写了预处理脚本,编译成二进制,跑一次只要0.3秒。

**第八个坑:推荐列表更新不及时**

改了旧文章的标签,推荐列表不会自动更新。我在脚本里加了个文件修改时间检测,只有内容变了才重新计算。

**第九个坑:中文分词问题**

标题里的中文词,直接用空格分词会切成单字。引入 jieba 分词,把标题和摘要里的中文词组提取出来,相似度计算才正常。

**第十个坑:JSON 文件体积**

400篇文章的推荐关系,JSON 文件有80KB。虽然不大,但每次页面加载都要请求一次。改成把 JSON 内联到 Hugo 的 data 目录,构建时直接编译进站点,省了一次 HTTP 请求。

**第十一个坑:缓存失效**

部署到 Vercel 后发现,修改了推荐列表但用户浏览器缓存了旧数据。给静态资源加了版本号,每次构建自动更新。

**最终成果**

推荐模块上线后,站内页面浏览量提升了37%,跳出率从78%降到了61%。最意外的是,读者平均停留时间从2分10秒增加到了3分45秒。

**踩坑总结**

1. 静态站做动态功能,预处理比运行时更靠谱,性能差10倍以上
2. 标签推荐太粗糙,一定要加文本相似度加权
3. 移动端适配不是事后补的,一开始就要考虑
4. 缓存策略要提前想好,不然改个标签要等一周才能生效
5. 中文站记得分词,不然“人工智能”和“人工智障”会被当成一样

代码已经开源在 GitHub 上了,repo 叫 `hugo-related-posts`,star 数比我博客文章还少,欢迎去提 PR。下次再遇到这种“简单加个功能”的需求,我可能会先掂量掂量——11个坑,一个比一个深。


📎 延伸阅读

看完这篇,如果你想:

  • 直接拿工具 → 回复”13“,我把跨境获客工具包发给你
  • 系统学习 → 点击菜单”AI训练营”,从0开始跑通AI变现

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注