Hexo 博客收口型优化实战:信息架构重组与内容治理全记录

前言

ByteFisher 博客从最初的 Hexo 基础静态站一路迭代,到现在已经覆盖了技术教程、钓鱼日记、钓鱼相册、鱼获 Dashboard、钓点地图、抖音视频、AI 小游戏、AI 问答助手、热门文章、PWA 等多个功能模块。文章总量突破 150+,线下工具脚本接近 20 个,第三方服务依赖 6 个以上。

功能越来越多,但问题也随之暴露:

  • 首页还是时间流——150 篇文章按时间排列,新访客不知道从哪开始看。
  • 文章摘要参差不齐——有的空着,有的截断,有的塞了正文片段。
  • 旧图床链接零星残留——以前用 iili.io,切到 img.bytefisher.top 后还有漏网之鱼。
  • tools 目录积灰——脚本越来越多,三分之一已经不再使用。
  • 基础设施欠账——PJAX cacheBust 没开、PWA 缓存策略过时、CI/CD timezone 没设。

本轮优化的核心理念是”收口”:不追加新功能,对已上线的内容、配置、代码做系统性清理。本文记录这次优化的完整过程。


一、信息架构重组:从时间流到入口矩阵

1.1 背景:一个首页只有时间线的博客

优化前的 index.swig 结构很简单:

1
2
3
4
5
6
7
{% block content %}
{% include '_partials/hero.swig' %}
{%- for post in page.posts.toArray() %}
{{ partial('_macro/post.swig', {post: post, is_index: true}) }}
{%- endfor %}
{% include '_partials/pagination.swig' %}
{% endblock %}

Hero 区域放了几个跳转按钮和技术统计,下面就是按日期排列的文章列表。对于知道自己在找什么的老读者没问题,但新访客面对 150 篇文章的时间流,往往不知所措。

1.2 设计思路:按入口引导 + 专题推荐

参考一些内容型网站的布局方式,把首页从上到下拆成四个区域:

  1. Hero 品牌区——保留头像、打字动画和全站数据统计
  2. 内容入口 Grid——教程、钓鱼、游戏、视频四张卡片,按主题分流
  3. 精选专题——游戏开发、C# 基础、Hexo 实战、AI 编程四个系列推荐
  4. 最新文章 + Featured 推荐

关键是第二和第三区——它们让新访客在 1 秒内理解”这个博客有什么”,并且两次点击就能进入任意核心系列。

1.3 实现:home-sections.swig

新增 themes/next/layout/_partials/home-sections.swig,内容区按模块切分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
{# 内容入口 — 四张卡片按主题分流 #}
<section class="home-section home-entry-section">
<div class="home-section-head">
<h2 class="home-section-title">内容入口</h2>
<p class="home-section-desc">按主题进入,少翻时间线。</p>
</div>
<div class="home-entry-grid">
<a class="home-entry-card home-entry-tech" href="/tutorials/">
<span class="home-entry-icon"><i class="fa fa-book"></i></span>
<span class="home-entry-body">
<strong>教程系列</strong>
<span>Unity3D、C#、Lua、Python、AI 编程</span>
</span>
</a>
<a class="home-entry-card home-entry-fish" href="/fish/">
...
</a>
<a class="home-entry-card home-entry-game" href="/ai-games/">
...
</a>
<a class="home-entry-card home-entry-video" href="/douyin/">
...
</a>
</div>
</section>

{# 精选专题 — 四个系列直接推荐 #}
<section class="home-section home-topic-section">
<div class="home-section-head">
<h2 class="home-section-title">精选专题</h2>
<p class="home-section-desc">从代表性系列开始阅读。</p>
</div>
<div class="home-topic-grid">
<a class="home-topic-card" href="/tags/series-Unity3D开发基础教程/">
<span class="home-topic-label">游戏开发</span>
<strong>Unity3D 开发基础教程</strong>
<span>从场景、脚本、物理到构建发布。</span>
</a>
{# ... 其余三个卡片类似 #}
</div>
</section>

对应的 CSS 全部写在 source/_data/styles.styl 中,采用 Grid 布局实现响应式卡片:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
.home-entry-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 16px;
margin-top: 20px;
}

.home-entry-card {
display: flex;
align-items: center;
gap: 12px;
padding: 16px 20px;
border-radius: 10px;
background: var(--card-bg);
border: 1px solid var(--border-color);
transition: transform 0.2s, box-shadow 0.2s;

&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
}
}

新增 featured-posts.swig,逻辑分两层:

1
2
3
4
5
6
7
{% for post in site.posts.sort('date', -1).toArray() %}
{% if post.featured and featured_count < 4 %}
{# 渲染 featured 卡片 #}
{% endif %}
{% endfor %}

{# 如果没有 featured 文章,退回显示最新 4 篇 #}

也就是说,只要在文章的 front-matter 中设置 featured: true,它就会自动出现在首页的”推荐阅读”区域。没有手动设置的文章,则自动拿最新 4 篇填充——完全不增加维护成本。

1.5 全站数据联动:教程计数打通 Hero、Sidebar、菜单

为了让 Hero 区域的统计数据真实反映博客内容,需要把教程系列数量透传到前端。原来这些数字是手写在配置里的。

themes/next/scripts/ 中新增教程计数逻辑,通过 Hexo 的 filter 在生成阶段自动计算:

1
2
3
4
5
6
7
8
9
10
11
// themes/next/scripts/filters/tutorials-data.js
hexo.extend.filter.register('after_init', function() {
var series = hexo.locals.get('posts').find({
'tags': { $regex: '^series-' }
}).map(function(post) {
return post.tags.filter(function(t) { return t.name.startsWith('series-'); });
});
// 去重计数
var uniqueSeries = [...new Set(series)];
hexo.theme.config.tutorialCount = uniqueSeries.length;
});

这样 Hero stat、侧边栏 site-overview、菜单 badge 都能用 {{ theme.tutorialCount or 0 }} 获取真实数据,不再需要手动维护。

效果:首页从纯时间流变成有层次的信息入口,新访客可以在两次点击内进入任意核心系列。后续统计显示,教程页的访问占比提升了约 30%。


二、SEO 与内容治理:14+ 篇文章摘要重写

2.1 问题:description 质量参差不齐

经过前几轮优化,文章 description 的覆盖率已经很高(95% 以上),但质量不行。

rg -n "^description:" source/_posts -g "*.md" 逐一检查,发现几类典型问题:

问题类型 示例 影响
空描述 description:(仅字段名,无内容) 搜索引擎用正文前 160 字,可能截断不完整
截断残片 以半句话结尾,有明显断句痕迹 搜索结果摘要读不通
正文污染 混入图片描述、代码片段、链接 URL 降低搜索引擎对页面主题的判断
过长 超过 160 字,搜索引擎也会截断 关键信息可能被截掉

2.2 工具:frontmatter-audit.py

写了一个审计脚本 tools/seo/frontmatter-audit.py,自动扫描所有文章的 front-matter:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
POSTS_DIR = Path(__file__).resolve().parents[2] / "source" / "_posts"
MIN_DESCRIPTION_LEN = 40
MAX_DESCRIPTION_LEN = 160

def find_issues(path, fm):
issues = []
desc = normalize_description(fm.get("description", ""))

if not desc:
issues.append("description 为空")
elif len(desc) < MIN_DESCRIPTION_LEN:
issues.append(f"description 过短({len(desc)} 字)")
elif len(desc) > MAX_DESCRIPTION_LEN:
issues.append(f"description 过长({len(desc)} 字)")

# 检查是否包含 Markdown 标记、``` 代码块、图片描述等污染
pollution_patterns = [
(r'!\[.*?\]\(.*?\)', '图片 Markdown'),
(r'```', '代码块'),
(r'http[s]?://', 'URL'),
]
for pattern, label in pollution_patterns:
if re.search(pattern, desc):
issues.append(f"含有 {label}")
break

return issues

运行一次就会列出所有有问题的文章,逐一修复。这个脚本只读不改,可以放心集成到发布检查流程中。

2.3 重写策略:按文章类型定模板

针对不同类型的文章,制定了对应摘要模板:

  • 技术教程[系列名] + [这篇文章教什么] + [读者收益]
  • 钓鱼日记[时间] + [地点] + [鱼获亮点] + [经验总结]
  • Hexo 实战[解决的问题] + [方案简介] + [涉及技术/工具]
  • 游戏开发[游戏类型] + [开发技术] + [核心功能点]

示例——之前 C#学习笔记【结构体Struct】 的描述是空字段,重写后:

1
description: 'C# 学习笔记系列第 5 篇。讲解结构体(struct)的值类型特性、与类的区别、适用场景和最佳实践,包含栈上分配和装箱拆箱的性能要点。'

14+ 篇文章的描述逐一按这个标准重写,同时文中关键词也同步做了归并统一。


三、废弃资产清理

3.1 Program 页面:停用废弃布局

source/program/index.md 使用了 layout: "program",这是一个历史遗留的自定义布局,已经被新搭建的 /tutorials/ 教程总入口页面取代了。但旧 URL /program/ 可能还在搜索引擎索引中,访问后返回一个不再维护的页面。

处理方式不是直接删除,而是改为静态跳转页:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
---
layout: "program"
title: 教程索引
noindex: true
sitemap: false
canonical: /tutorials/
---

<!-- 跳转提示 -->
<link rel="canonical" href="/tutorials/">
<noscript>
<meta http-equiv="refresh" content="0; url=/tutorials/">
</noscript>

<script>
window.location.href = '/tutorials/';
</script>

<div style="text-align:center;padding:100px 20px;">
<p>教程页面已迁移,正在跳转到 <a href="/tutorials/">新教程页</a>...</p>
</div>

加上 canonicalnoindex,搜索引擎知道该页面已不再独立索引。

3.2 旧图床链接清零

两个月前把钓鱼相册和文章配图从 iili.io 迁移到了自建的 img.bytefisher.top CDN,但 source/douyin/videos.json 中还残留着 22 条旧链接。

rg "iili\.io" source 快速定位所有残留,逐条迁移:

1
2
3
4
5
6
7
8
# 检查旧图床残留
rg "iili\.io|freeimage" source

# 结果:只有 douyin/videos.json 有残留
source/douyin/videos.json
12: "cover": "https://iili.io/xxx.jpg"
18: "cover": "https://iili.io/yyy.jpg"
...

将 22 条封面链接全部替换为 https://img.bytefisher.top/douyin/xxx.webp,再次运行 rg 零结果。

3.3 Tools 目录瘦身:一次性删除 16 个历史文件

这是最”爽”的一步。tools/ 目录经过多轮迭代,积累了大量不再使用的脚本:

  • archive/js/ 下的 download_images.jsdownload_compress_images.jsdownload_missing.jsfix_webp_urls.jsreplace_image_urls.js——这些是图床迁移时的临时工具,迁移完成后再无用途
  • archive/python/ 下的 fix_series_brackets.pyfix_series_format.pyfix_series_indent.py——历史格式修复脚本,修复过一次后不再需要
  • 整个 wechat-path/ 目录——公众号图片路径处理工具,对应的逻辑已经在主同步工具 wechat-sync/index.js 内实现了

清理了 16 个文件、约 800 行代码,tools/ 目录从杂乱状态变得清爽:

1
2
3
4
5
6
7
tools/
README.md
requirements.txt
seo/ # SEO 审计工具(新)
douyin-convert/ # 抖音视频转换
wechat-sync/ # 公众号同步
图片重命名工具/

四、基础设施加固

4.1 PJAX CacheBust:修复文章消失 Bug

线上发现一个诡异问题:新发布的文章”ByteFisher AI 编程实战(二)”首次加载正常显示,切换到其他页面再回主页就消失了,刷新后又出现。

追到 themes/next/layout/_scripts/pjax.swig 发现 cacheBust: false。PJAX 的 XHR 请求不会加时间戳参数,浏览器直接返回了 HTTP 缓存中的旧首页 HTML——那里面没有新文章。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 修改前
var pjax = new Pjax({
selectors: [...],
cacheBust: false, // ← 这就是问题
...
});

// 修改后
var pjax = new Pjax({
selectors: [...],
cacheBust: true, // 每次 XHR 带 ?t=Date.now() 时间戳
...
});

同时加了一道 opacity 安全兜底,防止 motion 动画未完成时 post-block 卡在不可见状态:

1
2
3
4
5
6
// 在 pjax:success 处理器末尾
document.querySelectorAll('.post-block, .pagination, .comments').forEach(function(el) {
if (parseFloat(el.style.opacity) === 0) {
el.style.opacity = 1;
}
});

4.2 PWA Service Worker 缓存策略升级

Service Worker 之前用的 CACHE_NAME = 'bytefisher-v4' 硬编码版本号,每次更新都需要手动改。改为自动生成:

1
2
// source/sw.js
var CACHE_NAME = 'bytefisher-' + Date.now();

缓存策略也从单一的 cache-first 改为分级策略:

1
2
3
4
5
6
7
8
9
10
11
12
13
self.addEventListener('fetch', function(event) {
var req = event.request;
var isHTML = req.headers.get('Accept').indexOf('text/html') !== -1;
var isCSS = /\.css(\?|$)/.test(req.url);

if (isHTML || isCSS) {
// HTML 和 CSS:网络优先,保证最新
event.respondWith(networkFirst(req));
} else {
// JS、图片、字体:stale-while-revalidate
event.respondWith(staleWhileRevalidate(req));
}
});

HTML 和 CSS 采用网络优先策略保证内容时效性;JS、图片、字体采用 stale-while-revalidate(同时用缓存响应和发起网络更新),兼顾速度与更新。

4.3 CI/CD 定时构建与时区

GitHub Actions 的定时任务之前没有设置时区,凌晨触发的时间点不符合预期。在 workflow 文件中加入环境变量:

1
2
3
4
5
6
env:
TZ: Asia/Shanghai

schedule:
- cron: '5 16 * * *' # 每天北京时间 00:05 触发
- cron: '0 2 * * *' # 每天北京时间 10:00 二次构建(兜底)

第一个 cron 在午夜过 5 分钟触发,目的是让未来日期文章(比如当天 0 点定时发布的文章)被自动构建上线。第二个作为白天的容错构建。

4.4 明确未来文章发布策略

博客有一个系列 ByteFisher AI 编程实战教程,从第 1 篇到第 21 篇预写了全部文章,日期跨度从 2026-06-08 到 2026-08-17。主配置 _config.yml 保持 future: false,所以普通 npm run build 不会生成这些未来文章。

但项目中又存在 build:allserver:all 脚本用于开发预览。如果不明确说明,容易导致两个问题:

  • 开发时看不到未来文章,以为构建出错
  • 生产构建意外包含了不应该发布的内容

在 AGENTS.md 中补充说明:

1
2
3
4
5
6
### 未来文章发布策略

- `_config.yml` 中保持 `future: false`,正式发布不生成未来日期文章。
- 不要为了预览未来文章而修改 `_config.yml``future` 配置。
- 需要提前发布某篇未来文章时,优先修改该文章 `date` 为实际发布日期,再运行常规构建。
- 定时发布的未来文章由 CI/CD 在每天 00:05(北京时间)自动构建上线。

这样每个环节都有明确的行为预期。


五、工具链优化

5.1 WeChat Sync:40007 错误与章节截断

公众号同步工具 tools/wechat-sync/index.js 在同步长文时遇到了两个问题:

问题一:40007 错误

调用微信 API updateDraft 时传参格式错误:

1
2
3
4
5
// 错误写法——返回 40007 invalid args
client.updateDraft(existingMediaId, article);

// 正确写法——需要用数组包裹
client.updateDraft(existingMediaId, [article]);

一行括号的差别,排查了半天。

问题二:长文章截断后标签不闭合

微信图文消息有正文长度限制,超长文章需要截断。但原来的 truncateHtml() 函数只按字符数切,切完后的 HTML 片段可能不闭合标签,导致微信渲染出错。

改为语义截断——按 <h2> 章节边界截断:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function truncateAtSection(html, maxLen) {
// 按 h2 分节,保留完整章节
var sections = html.split(/(?=<h2\b)/i);
var result = [];
var total = 0;

for (var i = 0; i < sections.length; i++) {
if (total + sections[i].length > maxLen) break;
result.push(sections[i]);
total += sections[i].length;
}

// 自动闭合所有未闭合的标签
return closeAllTags(result.join('')) +
'\n\n<p style="color:#999;font-size:14px;">📖 阅读完整内容请访问原文...</p>';
}

这样即使截断,每段导出的内容也是语法完整的 HTML。


六、总结

这次收口型优化前后涉及了 30+ 个文件的修改,横跨信息架构、SEO、废弃资产、基础设施、工具链五个维度。几个核心数据:

维度 内容
新增 首页内容入口 Grid + 精选专题 + Featured Posts
重写 14+ 篇文章描述,按系列统一摘要模板
清理 16 个无用脚本(800+ 行代码)、22 条旧图床链接、1 个废弃页面
修复 PJAX cacheBust、WeChat 40007、Service Worker 版本号
新增工具 frontmatter-audit.py(SEO 审计,只读)
文档 收口文档、未来文章发布策略、发布检查流程

下一轮(P3)重点将转向旧文翻新和 RSS/Sitemap 定期校验——但那是另一个故事了。


参考

ByteFisher
分享编程技术 · 记录钓鱼乐趣
扫码关注
▸ 扫码关注 ◂
分享: