Hexo 博客二次优化实战:从性能压缩到品牌打磨全记录

前言

ByteFisher 博客第一期优化(RSS 修复、AI 助手、评论迁移等)完成后,博客能正常跑了,但离”好用”还有距离。

梳理了一下发现几类问题:

  • 配置层:Google Analytics 的 tracking ID 填的是 G-XXXXXXXXXX——占位符,从未生效。About 页面还在说”评论系统:LeanCloud”——实际上早换了 Waline。
  • 代码层lightbox-plus-jquery.min.js每个页面加载,包括没有图片的文章页。50KB 的脚本白白传输。
  • 性能层:CSS/JS 压缩没开,外部域名没 preconnect,移动端侧边栏不可见。
  • 功能层:没有分享按钮、没有 PWA、侧边栏没热门文章、深色模式只能跟随系统。
  • 品牌层:Favicon 是 Next 主题默认的,Footer 底部还挂着 “Powered by Hexo & NexT”。

本文记录第二期优化的完整过程,从清理负债到品牌打磨。


一、清理与修复

1.1 Baidu Analytics 替代 Google Analytics

检查 themes/next/_config.yml 时发现:

1
2
google_analytics:
tracking_id: G-XXXXXXXXXX # 占位符,从未配置

这个配置自博客创建以来就是占位符。博客主要面向中文用户,Baidu Analytics 比 Google Analytics 更适合。替换方式很简单:

1
2
3
4
5
6
7
8
# themes/next/_config.yml
# 注释掉 GA
# google_analytics:
# tracking_id:
# only_pageview: false

# 启用百度统计
baidu_analytics: dc50e0997d43c3a8dbf2afdb3fdff233

一行配置,全站自动注入百度统计脚本。

1.2 About 页面过时内容

打开 About 页面,发现三处过时信息:

  • “评论系统:LeanCloud” → 实际已迁移到 Waline
  • “图床服务:FreeImageHost” → 实际在用 iili.io
  • 服务列表缺少 AI 助手、钓鱼特色功能等新功能

更新后的 About 页面加入了博客功能清单,包括 Waline 评论、AI 问答助手、钓鱼 Dashboard 和地图等。

1.3 lightbox 条件加载

这是这轮修复中最有”故事”的一个。

博客启用 mediumzoom: true 实现图片点击放大,同时也加载了 /js/lightbox-plus-jquery.min.js(约 50KB)。两个功能重叠,lightbox 仅钓鱼相册需要。

查看 _layout.swig,lightbox 是在全局加载的:

1
2
<!-- _layout.swig:11 — 全站加载 -->
<script type="text/javascript" src="/js/lightbox-plus-jquery.min.js"></script>

改成条件加载:

1
2
3
{% if page.layout === 'fish' %}
<script type="text/javascript" src="/js/lightbox-plus-jquery.min.js"></script>
{% endif %}

踩坑:改完后本地浏览首页,控制台报 lightbox 的 JS 错误(因为 lightbox-plus-jquery.min.js 在非相册页不再加载,但对应的 CSS /css/lightbox.min.css 还在全局加载)。

解决方法:CSS 也改成条件加载:

1
2
3
4
{% if page.layout === 'fish' %}
<link rel="stylesheet" type="text/css" href="/css/lightbox.min.css">
<script type="text/javascript" src="/js/lightbox-plus-jquery.min.js"></script>
{% endif %}

但改完之后首页仍然在报 404(找不到 /css/lightbox.min.css)。排查了半小时,发现是没有 hexo clean —— 旧缓存里还有 lightbox 的引用。


二、性能微调与体验优化

2.1 一行配置省 30%

1
2
# themes/next/_config.yml
minify: true

就这么一行。Hexo 生成的 CSS/JS 会移除空白和注释,体积减少约 30%。

minify 修改后必须 hexo clean && hexo g 才会生效。

2.2 Preconnect 预连接关键域名

博客依赖的外部域名有 4 个:Waline 评论、iili.io 图床、Google Fonts、jsDelivr CDN。不加 preconnect 的话,浏览器要先做 DNS 查询再建立 TLS 连接,每个域名浪费几百毫秒。

1
2
3
4
5
<!-- _layout.swig 的 <head> 中 -->
<link rel="preconnect" href="https://waline.bytefisher.top">
<link rel="preconnect" href="https://iili.io">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://cdn.jsdelivr.net">

小踩坑:第一次加到了 body-end.swig 里——preconnect 必须放在 <head> 才有效,放在 body 尾已经错过连接时机了。

2.3 其他配置优化

几个改动都很直接:

1
2
3
4
5
6
7
8
# 移动端侧边栏(手机也能看到 TOC 和站点信息)
sidebar:
onmobile: true
display: post

# 文章目录默认展开(不用手动点开)
toc:
expand_all: true

归档页加了统计摘要:

1
2
3
4
5
6
7
<div class="archive-summary">
<span>共 {{ site.posts.length }} 篇文章</span>
<span>|</span>
<span>最新更新:{{ site.posts.first().date.format('YYYY-MM-DD') }}</span>
<span>|</span>
<span>最早文章:{{ site.posts.last().date.format('YYYY-MM-DD') }}</span>
</div>

标签页从笨重的 tagcloud(字号忽大忽小)改为卡片式平铺:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.tag-cloud-tags
display: flex
flex-wrap: wrap
gap: 10px
justify-content: center

a
display: inline-flex
padding: 6px 16px
border-radius: 20px
background: #f5f5f5
transition: all 0.3s

&:hover
background: #37c6c0
color: #fff
transform: translateY(-2px)

三、结构化数据与 SEO

3.1 JSON-LD 结构化数据

搜索引擎要通过结构化数据才能生成富摘要(Rich Snippet)。之前的文章页完全没有 JSON-LD。

写了一个自定义 Helper(themes/next/scripts/helpers/json-ld.js,129 行):

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
'use strict';

hexo.extend.helper.register('json_ld', function() {
var schema = [];

// 首页:WebSite Schema
if (this.page.__index) {
schema.push({
'@context': 'https://schema.org',
'@type': 'WebSite',
name: this.config.title,
description: this.config.description,
url: this.config.url
});
}

// 文章页:Article Schema
if (this.page.layout === 'post') {
schema.push({
'@context': 'https://schema.org',
'@type': 'Article',
headline: this.page.title,
description: this.page.description || this.page.excerpt || '',
author: {
'@type': 'Person',
name: this.config.author
},
datePublished: this.page.date.toISOString(),
dateModified: this.page.updated.toISOString(),
mainEntityOfPage: {
'@type': 'WebPage',
'@id': this.page.permalink
},
publisher: {
'@type': 'Person',
name: this.config.author
}
});
}

// 分类/归档页:BreadcrumbList Schema
if (this.page.categories && this.page.categories.length) {
var items = [{ '@type': 'ListItem', position: 1, name: this.config.title }];
this.page.categories.forEach(function(cat, i) {
items.push({ '@type': 'ListItem', position: i + 2, name: cat });
});
schema.push({
'@context': 'https://schema.org',
'@type': 'BreadcrumbList',
itemListElement: items
});
}

return schema.map(function(s) {
return '<script type="application/ld+json">' +
JSON.stringify(s, null, 2) + '</script>';
}).join('\n');
});

post.swig 和首页模板中调用:

1
{{ json_ld() }}

涵盖三种 Schema:首页的 WebSite、文章页的 Article、分类页的 BreadcrumbList。部署后通过 Google Rich Results Test 验证通过。


四、特色功能增强

4.1 精选文章模块

首页 Hero 区域下方直接是文章列表,缺少推荐入口。新增”推荐阅读”模块,文章在 front-matter 中标记 featured: true 即可出现在首页推荐位。

1
2
3
4
5
6
7
8
9
10
11
12
13
{% if theme.featured_posts and theme.featured_posts.enable %}
<div class="featured-section">
<h3 class="featured-title">📖 推荐阅读</h3>
<div class="featured-grid">
{% for post in site.posts.find({featured: true}).limit(4) %}
<a href="{{ url_for(post.path) }}" class="featured-card">
<span class="featured-card-title">{{ post.title }}</span>
<span class="featured-card-date">{{ post.date.format('YYYY-MM-DD') }}</span>
</a>
{% endfor %}
</div>
</div>
{% endif %}

4.2 文章分享按钮

不需要引入任何第三方 SDK,纯链接实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<div class="post-share">
<span class="post-share-label">分享:</span>
<a href="https://service.weibo.com/share/share.php?url={{ page.permalink | urlencode }}&title={{ page.title | urlencode }}"
target="_blank" rel="noopener" title="分享到微博">
<i class="fab fa-weibo"></i>
</a>
<a href="javascript:void(0)" onclick="copyCurrentUrl()" title="复制链接">
<i class="fa fa-link"></i>
</a>
</div>

<script>
function copyCurrentUrl() {
navigator.clipboard.writeText(window.location.href);
}
</script>

4.3 PWA 支持

三部曲:① manifest.json ② Service Worker ③ 注册。

manifest.json

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"name": "ByteFisher",
"short_name": "ByteFisher",
"description": "钓鱼爱好者的编程世界",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#37c6c0",
"icons": [
{ "src": "/images/android-chrome-192x192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "/images/logo.svg", "sizes": "any", "type": "image/svg+xml" }
]
}

Service Worker(简约版,缓存首页 + 主 CSS/JS):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// source/sw.js
const CACHE_NAME = 'bytefisher-v1';
const urlsToCache = ['/', '/css/main.css', '/js/main.js'];

self.addEventListener('install', function(event) {
event.waitUntil(
caches.open(CACHE_NAME).then(function(cache) {
return cache.addAll(urlsToCache);
})
);
});

self.addEventListener('fetch', function(event) {
event.respondWith(
caches.match(event.request).then(function(response) {
return response || fetch(event.request);
})
);
});

注册脚本在 body-end.swig 中:

1
2
3
4
5
<script>
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js');
}
</script>

踩坑记录:manifest.json 一开始配置的图标路径是 /images/favicon-32x32.png,但 PWA 要求至少有 192x192 的图标才能触发”添加到桌面”。重新导出了 android-chrome-192x192.png,并把 theme_color 统一为品牌色 #37c6c0

4.4 热门文章排行(四连环踩坑)

侧边栏想加一个”热门文章”模块,展示阅读量最高的 5 篇文章。数据源来自 Waline 的 Counter 表(记录了每篇文章的阅读次数)。

第一坑:CORS——页面用 fetch 直接调 Waline API:

1
fetch('https://waline.bytefisher.top/api/article?limit=5&sort=hot')

浏览器报跨域错误。一开始以为是 Waline 服务端没配 CORS,查了一圈发现 Waline 默认允许跨域,但我的 Vercel 配置没正确传递 origin 头。

第二坑:表名——Waline 默认的阅读量数据存在 WalineVisitors 表,但我部署时用了自定义表名 Counter。API 请求里没有指定表名参数,导致返回空数据。

第三坑:字段名——Counter 表的字段是 time(阅读次数),但 Waline API 文档里用的是 count。API 返回 { time: 123 } 但前端代码读的是 item.count,一直显示 undefined

第四坑:PJAX 事件绑定——热门文章列表渲染后挂在侧边栏,但 NexT 的 PJAX 导航会把侧边栏替换掉。第一次加载能看到热门文章,PJAX 跳转后就没了。

修复方式是在渲染后调用 window.pjax.refresh(element)

1
2
3
4
5
6
7
.then(function(data) {
var html = '<ul>' + data.map(function(item) {
return '<li><a href="' + item.url + '">' + item.title + '</a></li>';
}).join('') + '</ul>';
document.getElementById('hot-posts-list').innerHTML = html;
window.pjax && window.pjax.refresh(document.getElementById('hot-posts-list'));
});

最后考虑到 Waline API 可能不稳定,改成了构建时生成静态 JSON 的方案——在 hexo g 阶段从 LeanCloud 拉取阅读量,写入 api/hot-articles.json,前端直接 fetch 静态文件,不再依赖运行时 API:

1
2
3
4
5
6
// scripts/generators/hot-articles.js
hexo.extend.generator.register('hot-articles', function(locals) {
// 在构建时抓取阅读量数据
// 聚合文章标题和 URL
// 输出 api/hot-articles.json
});

4.5 其他功能

深色模式手动切换——加了左下角的圆形切换按钮,点击后在 dark-mode class 和 localStorage 之间切换,刷新后保留设置。

代码块语言标签——用 CSS ::after 伪元素从父容器的 data-lang 属性读取语言名称并显示在代码块顶部:

1
2
figure.highlight
position: relative

AI 助手 RAG 增强——第一期实现的 AI 助手只能根据固定 prompt 回复,不知道博客到底有哪些文章。写了个 Hexo Generator,在构建时生成 api/posts-index.json(包含所有文章的标题、URL、标签、摘要),AI 助手在对话前先 fetch 这个索引,作为上下文注入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// scripts/generators/ai-index.js
hexo.extend.generator.register('ai-index', function(locals) {
var posts = locals.posts.sort('-date').map(function(post) {
return {
title: post.title,
url: post.permalink,
date: post.date.format('YYYY-MM-DD'),
tags: post.tags ? post.tags.map(function(t) { return t.name; }) : [],
categories: post.categories ? post.categories.map(function(c) { return c.name; }) : [],
excerpt: post.excerpt ? post.excerpt.substring(0, 200) : ''
};
});

return {
path: 'api/posts-index.json',
data: JSON.stringify({ posts: posts, total: posts.length }),
layout: false
};
});

五、品牌视觉打磨

5.1 Favicon 替换

NexT 主题默认的 Favicon 在 images/ 下有 favicon-16x16-next.pngfavicon-32x32-next.png,替换为自己的图标集后更新配置:

1
2
3
4
5
favicon:
small: /images/favicon-16x16.png
medium: /images/favicon-32x32.png
apple_touch_icon: /images/apple-touch-icon.png
safari_pinned_tab: /images/logo.svg

更新配置文件:

1
2
3
4
5
footer:
powered: false # 隐藏 "Powered by Hexo & NexT"
since: 2023 # 保留建站年份
icon:
color: '#37c6c0' # 心形图标改为品牌色

同时更新了 PWA manifest 中的图标路径,统一用新的品牌图标替换原来的 Next 默认图标。

5.3 文章 Meta 精简

之前的文章顶部 Meta 信息包含:分类、创建日期、更新日期、字数、阅读时长——5 样东西挤在一行,信息密度过高。

1
2
3
4
5
6
7
post_meta:
item_text: false # 隐藏文字标签,只保留图标
created_at: true
updated_at:
enable: false # 隐藏更新日期
another_day: true
categories: true

六、总结

整个第二期优化覆盖了 5 个阶段、30+ 项改动:

1
2
3
4
第二阶段: Baidu Analytics / About 更新 / lightbox 条件加载
第三阶段: minify 压缩 / Preconnect / 移动端侧边栏 / 目录展开 / 标签卡片化
第四阶段: 精选文章 / 分享按钮 / PWA / 热门文章(4坑) / 深色切换 / AI RAG
第五阶段: Favicon / Footer / Meta 精简 / 品牌色统一

几个核心经验:

  1. 配置是负债——占位符配置比没有配置更糟糕,它会让你以为某个功能在工作
  2. 改完要 clean——Hexo 的缓存很顽固,hexo clean 应该是肌肉记忆
  3. 热门文章的四连环坑最典型——小事不小,每个”小问题”单独看都是三分钟能修好的,但串在一起能消耗一个下午
  4. 构建时优于运行时——静态站的灵魂是把能提前算好的都提前算好,热门文章从 API 调用改为构建时生成,减少了运行时依赖故障点

后续还有图床迁移和 WebP 格式转换要做,留到第三期。

📖 查看 精选文章推荐
🔥 侧边栏热门文章模块已上线
🎣 查看 钓鱼相册

分享: