个人站点的技术债要定期还。这篇把 htovo.art(海豚の小窝)的技术栈摊开讲清楚,再复盘一下最近一个月我在这个站上动手做的一堆工程改动——既有性能优化,也有体检排雷,还有一次「断舍离」。既是给自己的笔记,也方便将来(或者未来的我)快速接手。
一、技术栈全景
站不大,但选型讲究「可控、能自己排障」。核心就一句话:前端框架扛渲染,浏览器内 SQLite 扛数据,Nginx 扛缓存和安全,AI 中转扛生图。
| 分层 | 选型 | 说明 |
|---|---|---|
| 前端框架 | Next.js 16(App Router)+ React 19 | RSC、流式渲染;文章页走静态生成 |
| 样式 | Tailwind CSS v4 | 原子化,零配置主题 |
| 数据存储 | SQL.js(浏览器内 WebAssembly SQLite) | 无后端数据库,DB 文件随站走 |
| 进程管理 | PM2 | blog 主进程 + hour-cache 采集 worker |
| 反向代理 / 缓存 | Nginx | 静态直出、强缓存、限流、统一安全头 |
| 图像处理器 | sharp | 缩略图压缩、JPEG 重编码(不花接口钱) |
| AI 能力 | gpt-image 系列(经 aisenyu 中转) | 文生图 / 图生图编辑 |
| 鉴权 | HMAC-SHA256 token + middleware | admin-auth,可一键开关 |
| 基础设施 | 腾讯云 Ubuntu VPS | Let's Encrypt HTTPS,cron 每日备份 |
几个关键点值得展开:
- 数据不上云。 全站没有独立后端数据库,状态都落在 SQL.js(WASM 版 SQLite)里,DB 文件直接跟着项目走。好处是部署极简、可离线;代价是重启最多可能丢 5 分钟数据,所以写盘频率压到了 5 分钟一次。
- 鉴权可一键熔断。
lib/admin-auth.ts里有一个AUTH_DISABLED开关,置true时verifyToken()无条件放行,12 个 API 路由的调用点零改动就能继续工作。当时前台密码门取消,靠的就是这个,而不是去逐个删鉴权代码。
// lib/admin-auth.ts —— 统一的登录态判断,调用点一律用这个
export async function checkAuth(token?: string | null): Promise<boolean> {
if (AUTH_DISABLED) return true; // 密码门已取消:直接放行
return !!(token && (await verifyToken(token)));
}
export async function verifyToken(token: string): Promise<boolean> {
if (AUTH_DISABLED) return true;
// HMAC-SHA256 验签 + TTL 校验
const parts = token.split('.');
if (parts.length !== 3) return false;
const [ts, rand, sig] = parts;
const expected = await hmacSign(getSecret(), `${ts}.${rand}`);
if (!timingSafeEqualHex(sig, expected)) return false;
if (Date.now() - parseInt(ts, 36) > TOKEN_TTL) return false;
return true;
}
二、近期工程记录
1. 模板预览图加速
问题。 image-studio 页每个模板卡片都直接拉原图,单张约 2.59 MB,整页十几张图同时加载,页面能卡到怀疑人生。
思路。 页面只负责「展示」,生图才需要「高清原稿」。所以:原图(库里的 .bin)一个字节不动,页面改拉一张用 sharp 现场压出来的小 JPEG。
做法。
lib/template-preview.ts用 sharp 把原图压成 640px / q72 / mozjpeg 的 JPEG,缓存到/var/www/tpl-preview/<id>.jpg,首请求按需生成,之后直接读缓存;- 页面
<img>加loading="lazy" decoding="async",并做三级onError兜底(静态图 → API 生成 → 原图); - Nginx 用
location ^~ /tpl-preview/静态直出,完全不经过 Node,并挂一年强缓存:
# nginx_blog.conf
location ^~ /tpl-preview/ {
alias /var/www/tpl-preview/;
add_header Cache-Control "public, max-age=31536000, immutable";
include /etc/nginx/snippets/security-headers.conf;
access_log off;
}
踩坑(重点)。 /home/ubuntu 是 750 权限,Nginx 的 www-data 用户穿不透,预览图一开始放用户目录里直接 403。解决办法是迁到 /var/www/tpl-preview/,目录属主 ubuntu:www-data、权限 2755——应用(ubuntu)可写,Nginx(www-data)可读。
结果。 单张预览图从 2.59 MB → 42 KB,降幅约 98%,配合懒加载,首屏不再是「转圈地狱」。
// lib/template-preview.ts —— sharp 本地压缩,不调接口、不花钱
const out = await sharp(tpl.buffer, { failOn: 'none' })
.resize({ width: 640, withoutEnlargement: true })
.jpeg({ quality: 72, mozjpeg: true })
.toBuffer();
2. 图像编辑能力实测
之前有「改图能不能用」的疑问,我直接跑了一遍图生图链路验证。结论是:功能本身可用,用的 gpt-image 系列模型(默认 gpt-image-2.5-flare),走 aisenyu 中转。真正的瓶颈不在代码——中转商在高峰时段会 502/503 过载,单次改图等待能到 3 分钟级。所以代码层能做的只是加好超时和降级,体验改善得等中转侧扩容。
3. 全站体检
对着线上扫了 41 条路由(覆盖页面、API、静态资源),结论是「大体健康,但有历史包袱」:
- 本地
git落后线上一截; - 备份脚本把
.git也裹进了打包,体积虚高; - 之前一个
hour-cache采集 worker 已经悄悄消失(被合并掉了)。
逐项处理:补 git、让备份排除 .git、确认 worker 合并无遗漏。
4. UI 走查与 dashboard 登录死路
用 Edge 跑了一次 UI 走查,抓到两个体验坑:
dashboard页给未登录访客弹「请先登录 / 点右上角登录」的死提示,但登录入口已经被砍掉了——进了死胡同;tools页写死了一个绿色「已登录」徽标,纯属假象。
处理:按需求取消前台登录门(AUTH_DISABLED = true),同时把这类假提示和假徽标清理掉,不补表单(老板说暂时不需要)。
5. 杂谈板块整体下线
「杂谈」这个栏目判定为无意义,做了一次彻底断舍离:
- 删路由(
/chatter整目录)、删数据(29 篇 md,先备份到haitun_backup_chatters_*再删)、删 20 个引用文件里的相关代码与统计块; - 最后清掉备份目录,以及一批含杂谈的旧 tar 快照,让
chatters计数归零; - 验证:扫 41 路由全部 200/404,
/chatter、/message均 404,sitemap 与 recent-updates 无杂谈残留。
三、几个反复踩的坑
把这些坑记下来,省得下次再交学费:
- Next.js 动态路由强制
no-store。 在代码里设的Cache-Control会被框架覆盖成no-store,代码层的缓存头基本不生效。要浏览器/边缘缓存头,必须走 Nginx 加。这也是为什么模板预览图要单独做静态 location 直出。 /home/ubuntu的 750 权限穿透问题。 任何想让 Nginx(www-data)直接读的用户目录内文件都会 403。经验法则:需要静态直出的目录,一律放/var/www并给 www-data 可读。- 部署走 scp 不经 git。 线上源码不是靠 git 拉取的,而是在本地改好补丁、scp 推上去。这带来「本地补丁工作流」——所有改动先在本地
haitun_patch/留底,再推服务器。
四、小结
这个站的技术栈不追求新潮,核心标准是三条:够用、可控、能自己排障。Next.js + 浏览器内 SQLite 把部署复杂度压到最低,Nginx 把缓存和安全一把梭,AI 中转把生图能力外挂进来。
最近这轮改动里,最值的的两笔是模板预览图加速(体验立竿见影)和杂谈板块清零(代码面更干净)。剩下的坑,下个月体检时再见分晓。
