重构我的博客
一次愉快的「还债」之旅
时隔两年多,我终于再次对博客下手了,之前的每次都是折腾博客内容样式、构建流程,本次我将重点放在了架构的整理上。得益于如今 Vibe Coding 的成熟,本次重构虽然工作量是「历史之最」,但对于我个人来说,却是我最享受的一次:结构化的梳理、复杂度的降低,以及部署的自动化。
各模块介绍
目前博客涉及到的所有服务大致可以分成 5 个模块,其中部分是在自己的 VPS 上托管,另外部分在其他的 SaaS 服务商托管。
内容
博客的内容仍然保持纯 Markdown 形式,因此构建输出的的也是纯静态文件,可以很方便托管在 Cloudflare Pages,不需要 Worker 或者 KV。
之前一直用的 Hugo + SolidStart 方案,Hugo 负责内容将 Markdown 渲染成 HTML,SolidStart 再提供页面的组件样式,这套流程很别扭——或者说我的实现很别扭:SolidStart 是 file-based 的路由,所以我在 src 目录做了一个软连接指向了 Hugo 生成的目录(都是 JSON 格式的文件),然后SolidStart 这边动态解析 JSON 文件然后还要对已经渲染成 HTML 的重新解析,对某些已渲染的内容做一些替换,比如:高亮/数学公式渲染等。而且这部分只能放在构建的流程做,所以我又写了几个 Vite 插件。
但是由于我博客的分类、RSS、站点地图等都是 Hugo 做的,用 SolidStart 来重写倒不是做不到,就是有些麻烦。加上我好不容易打通了这一整套流程,所以就一直保持着现状了。
评论
从建站后很长一段时间,我使用的是 Disqus,评论数很少,而且 Disqus 广告又多,加载又慢,我嫌弃太臃肿,原本的评论我也没删,导出之后会自动在博客评论区以只读形式展示。
2023 年 8 月,我将 Disqus 换成了 giscus,一款基于 GitHub Disscussion 的评论系统。两年时间用着倒是还行,但是前段时间 Next.js/RSC 爆发了非常严重的安全漏洞,我在排查的时候,发现了 giscus 也是基于 Next.js 开发的。好消息:giscus 使用的 Next.js 版本不在漏洞影响范围内;坏消息:12.x 版本早已被官方停止维护了。
giscus 或者是 utterances 都是基于 Github OAuth 认证的 + 在页面通过 iframe 嵌入,考虑用户在点击授权时,凭证都是存储在第三方应用服务器的,在我看来,始终还是有些不可控的隐患存在。
搜索
搜索模块是两年前才加上的:Tantivy(Rust 编写)作为搜索引擎,Golang 作为 API 服务通过 RPC 去调用,我当时还专门自学了 TLV 编码 + TCP 连接池保持二者的通信。之所以这么做,是因为当时我对 Rust 实在不熟悉,当时又有部分 API 是用 Golang 写的并且正常运行中,所以就正好合进来了。
这个模块确实是我有炫技的成分,把一个功能非常单一的组件和另一个基本上无关的组件捆绑了,初期调试一度非常困难:必须要同时启动两个服务,同时连接池的释放问题也出现过 bug。
点赞 + 豆瓣秀
这两部分是用 Elixir Phoenix + PostgreSQL 数据库。数据库就是使用的的 AivenCloud 的免费版方案,后端服务则是托管在 Gigalixir。
点赞组件也差不多上线运行了两年,之前一直没有介绍过,正好介绍下。点赞本身只能点一次,数据持久层是放在 PostgreSQL,同时热数据存在内存里(更准确地说是 ETS 表中),所有操作一律只针对内存,定期同步至数据库。
豆瓣秀则是一个爬虫,每天运行一次爬取个人收藏页面,入库,页面只请求今年以来的数据。
请求同样经由 API 网关转发给 Gigalixir 的服务。因为数据量小,Gigalixir 的免费版倒也够用,唯一的问题就是,免费版要求 30 天内必须提交一次更新/部署,不然就会被托管方停掉服务。
站点统计
这个模块是我目前博客最重的服务,用的是自部署的 Plausible 方案。它提供的功能还行,但是搭配的后端数据库太复杂了,同时使用了 PostgreSQL + Clickhouse,全套服务都是在我的服务器上运行。
Clickhouse 是典型的内存大户,应对大流量的 OLAP 很好用,但问题是我的博客每天就一百人访问,属实是用不上 Clickhouse。很可惜的是,Plausible 本身不提供只用 PostgreSQL 作为唯一数据库的轻量部署方案。
复杂度是一种债务
运行维护以上的各种服务,很显然并不轻松。
语言栈的分裂
博客内容的生成我就用了两种语言:Hugo 的模板语言 + JavaScript(Solidstart) ,后端就更多了+ Rust(tantivy)+ Go(API 服务)+ Elixir(Phoenix) + 两套 DB(PG + ClickHouse)。
对个人开发者来说,在开发时选择体验不同语言的新鲜与舒适感是一种隐形的超前消费,在运维时终究要加倍偿还。
部署位置的分裂
目前博客的前端部署在 Cloudflare Pages,后端却同时部署在好几个不同的地方:自建服务器(搜索/统计/SSR)+ Gigalixir(Phoenix)+ Aiven(数据库)。
尤其是搜索和点赞,这俩模块的链路是尤其长,之前排查过几次也是相当费劲。
Plausible 的重
这一点问题其实和前两点不一样,Plausible 本身用的是 docker compose 部署。唯一的问题是两套数据库(尤其是 ClickHouse)在运行层面带来的:占用运行内存大、占用磁盘空间也大,升级的风险也大。
与 ChatGPT、Gemini 反复进行了数次讨论,围绕着更工程化、好维护的目标,最终敲定了一套最适合我的重构方案。
前端重构
把博客当前的功能以及我的要求发送给了 Gemini,然后让它出具了几个不同的页面。不得不说,Gemini 3.0 在写前端样式真的比 Chatgpt 5.2 强太多。目前前端的内容构建流程已经去掉了 Hugo,所需功能全部由插件或者手写代替了。
设计风格
重新设计了更加统一的配色方案,只保留一个强调色(#1f6f6a)的同时,尽可能减少其他除了黑白之外颜色的出现。
重新设计了各组件(主要是链接)的交互方式:
- 顶部导航栏以及工具栏:仅 Hover 时切换到强调色;
- 首页的「继续阅读」、「查看更多文章」等跳转页面,加上了指引箭头,hover 时变强调色;
- 首页的文章标题,hover 仅加下划线,这里的标题字体比较大,如果 hover 时候变强调色,会显得很突兀;
- 底部页脚部分,严禁出现强调色,而是通过更浅一级的字体,hover 时候加深来体现可点击。
代码高亮
从 highlight.js 切换成了 shiki 方案,同样也是在构建期完成,shiki 的速度比 highlight.js 慢很多,但是精细度更佳。
同样为了确保与整体设计风格一致,没有采用现有的配色方案,而是让 shiki 只做语义化 Token 的标注:函数、常量、关键字、注释、操作符等等。配色统一使用 CSS 变量定义。
图片相关
- 去掉了所有非必要的图片:头像,以及文章封面图。提升加载速度的同时,也让我不用在写完文章之后纠结到底用什么配图了;
- 原先的 og:image 使用的是文章的插图,现在都改用了在构建期使用页面元数据绘制生成,会更像文章的封面;
- 重新设计了站点 favicon,换成了更具有个人品牌特色的图案。
评论
去掉了 Giscus 组件,现 GitHub Discussion和旧时的评论会由后端统一返回,前端统一渲染。对于缺少头像的评论(比如 Disqus),头像缺省使用名字的首字符。
其他
- 文章排版统一交给了 UnoCSS 的 Typography preset 方案。仅微调了字号和颜色;
- 文章侧栏的社交区块放在文章尾部,优先确保阅读体验;
- 还有很多小细节,就不一一列举了。
后端重构
后端方面主要就是和 ChatGPT 进行讨论了,仍然是先梳理出各模块的功能,合理化组织架构,再交由 Codex 实现。目前后端的服务全部使用 Rust 实现,分为两部分:API 和 Jobs(定期同步等任务)。数据库也减少到 1 个,均在我个人服务器上部署运行。
评论
关于评论模块该如何实现我很是纠结了一阵子,也详细调查了纯自建、纯托管等方案的优劣势,权衡之后,决定采用折中的方案——评论源还是采用 GitHub Disscussion,页面只做展示。由后端定时查询 GitHub Disscussion 存入数据库,文章页面底部的评论区只展示评论内容,不展示评论框,需跳转到 Github 参与评论。
我也在设计「点击跳转才能回复」时纠结了许久,认为这个额外的点击跳转步骤可能会对用户有些许不便。但最终还是采用了这个方案:可以显著减少组件实现的复杂度,也不需要用户点击授权才能回复,安全性也有保障;再就是,对于首次评论的用户,其实和之前 giscus 时回复的流程一样,无非是把跳转授权改成了跳转到 GitHub Disscussion 对应主题。
搜索
这模块改动是最小的,还是用的 Tantivy 方案,目前不再需要跨进程 RPC 调用了。
点赞 + 豆瓣秀
这部分逻辑比较很简单,直接重写成 Rust 版本了。
站点统计
不再使用 Plausible,改成自建统计方案,分为 2 个接口:
- /pv,首次进入页面即发送信号;
- /engage,页面变动:关闭、跳转前,通过 navigator.sendBeacon 发送浏览时长数据。
部署
也重新整理了一下部署相关的流程,目前全部使用 Podman Quadlet 方案完成自动化部署。这个方案实在是太方便了,我所有的服务现在都由 Quadlet 来统一管理部署:
- 设置代码仓库本身的 CI,比如:指定分支提交会触发 Github Action 的构建,推送镜像到 ghcr;
- 设置服务器端的 .container 配置文件,指定镜像为 ghcr 镜像,同时开启
AutoUpdate=registry;
这样,后续的功能修改就不需要手动管理了:
- 代码修改之后,推送到 Github,会触发 Github Actions 的构建,推送镜像到 ghcr.io;
- podman-auto-update 会定期(通常是每天半夜)检测所有通过 Quadlet 创建的容器是否有新镜像,并重启服务;
重启服务、查看日志等用的都是 systemd 的那套命令,真的极大减少了心智负担。