前言
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
|
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%
就这么一行。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
| <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
| 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 = [];
if (this.page.__index) { schema.push({ '@context': 'https://schema.org', '@type': 'WebSite', name: this.config.title, description: this.config.description, url: this.config.url }); }
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 } }); }
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 和首页模板中调用:
涵盖三种 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
| 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
| hexo.extend.generator.register('hot-articles', function(locals) { });
|
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
| 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.png 和 favicon-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 since: 2023 icon: color: '#37c6c0'
|
同时更新了 PWA manifest 中的图标路径,统一用新的品牌图标替换原来的 Next 默认图标。
之前的文章顶部 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 精简 / 品牌色统一
|
几个核心经验:
- 配置是负债——占位符配置比没有配置更糟糕,它会让你以为某个功能在工作
- 改完要 clean——Hexo 的缓存很顽固,
hexo clean 应该是肌肉记忆
- 热门文章的四连环坑最典型——小事不小,每个”小问题”单独看都是三分钟能修好的,但串在一起能消耗一个下午
- 构建时优于运行时——静态站的灵魂是把能提前算好的都提前算好,热门文章从 API 调用改为构建时生成,减少了运行时依赖故障点
后续还有图床迁移和 WebP 格式转换要做,留到第三期。
📖 查看 精选文章推荐
🔥 侧边栏热门文章模块已上线
🎣 查看 钓鱼相册