更新历史
版本号语义化:主.次.修订。每次改动在这里加一节。
修复:/admin/logs 里那句「只显示最近 $300 条」多了一个 $
正文里写 ${VISIT_LIMIT} 时,Astro 在这里把 $ 也原样输出了(渲染成 $300)。
页面上看起来就是「$300 条」,很怪。现在直接写字面量 300 并注明它来自 frontmatter
(<script> 里本来也只能写字面量)。
顺带记一条网络实况:站主这台机器的 Watt Toolkit 直接占 80/443、不提供代理端口,
Windows 系统代理是关着的 —— 所以 git push 直连就行,
~/.gitconfig 里那段指向 127.0.0.1:7890 的 per-URL 代理是残留、已删。
补齐:改名 / 归类 / 删除时,缩略图跟着一起搬、一起删
上一版只做到「上传时带缩略图」,其余三条路都漏了 ——
而站点是按「主图同名 .jpg」去找缩略图的(thumbPathFor),所以:
- 改名后 → 站点找不到缩略图,退回显示原图(不会破图,但变慢),图库还会标「原图」角标
- 删除后 →
thumbs/里留下孤儿(仓库里当时就有两个:167155.jpg、0c912…jpg)
现在四条路都对齐了:上传带缩略图、改名搬缩略图、归类(老图挪回根目录时)搬缩略图、 删除(单张 + 批量)连同队列提交一起清缩略图。都放在同一次提交里, 要么都在、要么都不在;没有缩略图的图不会被凭空补一个空文件。
顺手清掉了仓库里那两个孤儿缩略图,并给 npm run verify 加了一条守卫:
assets-src/ 里那两个留档原图必须还在(别顺手删了),也不许溜进 public/(那会 14.8MB 上线)。
测试(check-worker-admin 6.6 节)新增:带缩略图改名后缩略图在新名字下、旧名字下已删、
内容逐字节没变;没有缩略图时改名照样成功且不凭空造;删除时主图与缩略图一起没了。
新增:媒体页面点图片 → 直接全屏看原图
站主说媒体页里点图片要看大图。
(Decap 的卡片预览是 CSS 压到 160px 高的、object-fit:contain —— 图片文件是原图,
但看起来很小,这就是他要「点开看」的原因。)
- 媒体页点图片 → 全屏灯箱(用项目里已有的
dcx-mask灯箱,顶部显示文件名),Esc/ 点背景 / 「关闭」都能退,另有「在新标签打开原图」。 - 编辑器「选择图片」弹窗里 双击图片同样全屏 —— 那边单击=选中 (右侧才出现「插入」),绝不能吞掉,否则插不进图。
- 判据(这一版改对了):看路由。
#/media、#/medium/...算媒体页,#/collections/...一律放行给 Decap。
⚠️ 上一版判据是错的,所以功能等于没上线:我原本靠「工具栏里有没有『选择』文案」
来判断,线上判不中 → 单击被放过 → 站主在 PC 上点半天没反应。教训:
判据要拿真实页面量过再用;这次也确认了媒体页路由就是 #/media(本地实测),
并把 /medium/ 这条一并认了。
测试(check-decap-extras):媒体页点击 → 全屏且是点的那张、显示文件名、Esc 能关;
#/collections/... 下单击不接管(插图流程不受影响)、双击才全屏;#/medium/... 也认;
按钮里的图不被接管、点空白不误开。
更正:若娜瓦那张图的「网页用图」恢复到你自己的原图
上一版我擅自把它按「图库上传的压缩规则」缩到了 1600×899 / 139KB, 站主指出那是把缩略图级的画质当成了原图。已改:
| 用途 | 尺寸 | 大小 |
|---|---|---|
| 文章里默认显示(缩略图) | 720×405 | 28 KB |
| 点开看的原图 | 4096×2302 | 0.93 MB |
留档(assets-src/) |
10228×5748 | 3.9 MB(JPEG)+ 14.8 MB(逐字节原版 PNG) |
没有直接把 10228px / 14.8MB 设成网页用图:那一张会让点开的瞬间下载 15MB。 4096px 在任意屏幕上都够锐,1MB 也能接受。要更大的(6144px / 1.7MB)说一声就换。
修复:手机上点开正文图片是「一片空白」
站主报「点击图片显示原图错误,新页面是空白」。用真浏览器 + 手机视口复现后看清了: 灯箱直接把 src 换成原图,而原图现在有 1MB(慢网 50KB/s 要 19 秒)—— 等它的那几秒屏幕上就是纯黑,看起来就是「空白」。原图越大越像坏了。
现在改成三步(PostLayout.astro 的灯箱):
- 先铺缩略图(几十 KB,本地缓存里就有,立刻可见)
- 底下显示「正在加载原图…」
- 原图加载完淡入盖上去
顺带修掉一个只有缓存命中时才会出现的 bug:只写 pic.onload 是不够的 ——
原图命中缓存时,赋值那一刻就已经加载完了,load 事件不会再触发,
表现成「缩略图一直糊着、提示不收」。现在设完 src 再同步查一次 complete。
新增 tools/check-lightbox.mjs 并进 check:all(现在 11 项):
真浏览器 + 手机视口 + 50KB/s 限速,断言「点开后 0.5 秒内看到缩略图」「提示可见」
「原图没到时不显示它」「原图到了会自动盖上来」「关得掉」。
(这是个纯时序 bug,肉眼看代码看不出来,所以必须有测试盯着。)
2.18.3 · 2026-10-01
修复:后台改了代码,手机上还在跑旧 JS(缓存咬人)
站主报告「刚从手机传了 4 张 14MB 的 PNG,传上去还是 170KB 的 JPG」。
查证:服务端代码早就修好了 —— 线上 gallery.js 已无 shouldCompress,
ui.js 已无 toJpegName / canvas.toDataURL。压缩发生在手机浏览器本地的旧 JS 里:
/admin/g/ui.js 是无哈希文件名,默认 Cache-Control: max-age=14400,
所以手机最多能把旧代码用 4 小时 —— 在这期间上传,图会被本地压成 JPEG 再发上来,
服务端只是照单收下(它从来没收到过原始 PNG,所以也救不回来)。
根治(两招一起上):
- 三个后台页引用脚本都带上
?v=版本号(/admin/g|m|p/ui.js?v=2.18.3)—— URL 变了就绕开浏览器缓存 public/_headers给/admin/*加Cache-Control: public, max-age=0, must-revalidate—— 强制每次回源校验(有 ETag,没变就是 304,代价很小)
npm run verify 新增两条守卫,防止以后有人把这层保护去掉或忘了加版本号:
「后台脚本都带 ?v= 版本号」「/admin/* 强制回源校验」。
⚠️ 教训:改后台脚本后,光看线上文件是新的还不够 —— 用户设备可能还在跑缓存里的旧版。以后改这些文件必须同时把 ?v= 版本号加一。
2.18.2 · 2026-09-30
站主提了 4 件事,全部处理。
1. 图库不再把 PNG 转成 JPG(原图原样保留)
以前上传时有一条「超过 400KB 就转成 JPEG(最长边 1600)」的规则, 后果是上传 PNG 会被悄悄改成 .jpg。站主明确要求:原始图片原样留在画廊里,不做任何压缩。
现在 prepare() 只做一件事:把文件原样读成 dataURL 上传 ——
格式、尺寸、字节都不动(PNG 就是 PNG)。shouldCompress / toJpegName / MAX_EDGE 那套压缩逻辑已删除。
2. 只有「超过 1MB」才生成缩略图
门槛从旧的 400KB 改成 1MB(站主定的规则):
- ≤ 1MB:不生成缩略图,原图直出
-
1MB:另外生成一张 720px/q68 的缩略图(只是额外产物,主图永远是原图)
- 文章里默认显示缩略图,点开加载原图;gif/svg 不生成缩略图(会丢动画/本就更小)
3. 修:全屏图上盖着原位置的缩略图
灯箱是「缩略图占位 + 原图淡入」两层叠放。原图就绪后缩略图层没有退场,
于是它一直压在原图上面 —— 两张图尺寸不同时,露在外面的那一圈糊图特别明显。
现在原图就绪会同时给舞台加 is-ready,把缩略图层透明化并禁掉交互。
check-lightbox 加了两条断言盯住它(原图就绪 → ph 透明度归 0、不再接收点击)。
4. 修:/admin/p/ 的文章列表不跟着仓库更新(前几天的文章不出现)
根因:那份索引(存 KV 的 posts:index)只在第一次(KV 里没有)时建一次,之后再也不更新 ——
所以通过 Decap 后台或 GitHub 直接发的文章永远不出现在 /admin/p/ 里。
改法:抽出公用函数 buildPostIndex(),/admin/reindex 和 /admin/posts-index 共用;
读列表时顺手比一下文章数量(只读一次目录,不读文件内容,便宜),对不上就自动重建。
同一份索引最多每 60 秒查一次(INDEX_CHECK_SEC),避免频繁打 GitHub。
冷启动(KV 里没索引)也会现场建,不再要求用户手动点「重建」。
check-worker-admin 新增 6.93 一节真的跑一遍这条链路:仓库里多一篇 → 列表自动出现;
没变化时不重建(省配额);节流生效;冷启动能建出来。
2.18.1 · 2026-09-29
修复:大图被改名/归类时整张图变成 0 字节(真丢过一张图)
现象:站主在手机图库里把 167155.jpg(14.8MB)改名成「若娜瓦-低眉.jpg」,
结果仓库里那张图成了 0 字节 —— 文章里引用还在、线上却是 404,
图库一直显示「还没构建好」(其实永远等不到)。
当时那张图是 10228×5748、以 PNG 存成 .jpg 的,已从历史里恢复。
根因(这一条要记住):
GitHub 的 Contents API 对超过 1MB 的文件不返回 content ——
它给 encoding: "none"、size 是真大小、content 是空串。
ghGetBlob 直接 (data.content || '') 当内容用,于是「读一张大图」拿到空串,
再原样写回 → 0 字节 blob 覆盖了原图。
小图全都正常,所以光看代码完全不觉得有问题。
改法:ghGetBlob 发现「内容是空、但 size > 0」时,改用 sha 走
Git Blobs 接口取真正的 base64(那个接口没有 1MB 限制);
取不到就抛错,绝不把空内容写回去。
再加一道防线:ghCommitMany 里凡是「内容为空」的文件一律拒绝提交
(响亮地失败,而不是把用户的图抹掉)。
测试的坑(同样重要):
原来的假 GitHub 一律返回 content,于是这个真丢图的 bug 在测试里完全隐形。
现在桩照真的来 —— >1MB 的文件只给 sha / size、不给内容,
并且支持 GET /git/blobs/<sha>。新增用例:
大图改名后逐字节比对、拿不到内容时明确报错且原文件不动、源文件不存在时报 404。
2.18.0 · 2026-09-29
HANDOFF「待办三件」里做了 ② 和 ③,① 按站主要求先不做(/admin/m 列表改成直读 GitHub)。
② 文章增删改日志(永久保留)
站主要知道「谁在什么时候动了哪篇文章」。范围明确:只记新增 / 修改 / 删除,不记浏览。
- 记什么:动作、文章标题、可点击链接(
/posts/<slug>/)、IP、时间(+08:00)。 删除的文章已经没有链接了,那条的链接留空,界面就不把标题做成可点的。 - 存在哪:Cloudflare KV,两个键各管一件事 ——
plog:<毫秒时间戳>-<随机>是永久流水(?all=1走它,键前缀天然按时间排序),plog:recent是最近 200 条的缓存(界面打开只读这一个键,不用扫全量)。 ⚠️ 写日志时刻意不传expirationTtl—— 访问日志那套 90 天 TTL 是给浏览记录的,别混。 - 落点:
/admin/save(新增 / 修改,改之前的老标题也一起记)与/admin/delete(删除); IP 取CF-Connecting-IP。日志写入包了一层 try/catch:记日志失败绝不能让文章保存报错。 - 新接口
GET /admin/post-logs(BearerLOGS_TOKEN):默认最近 30 天 / 最多 200 条,?all=1给全部历史。 /admin/logs重做:主表是文章日志(只渲染最新的一屏),访问日志挪到下面那张表; 两边都能导出 CSV。以前一打开就把所有记录全渲染出来,记录一多就卡。
③ 新上传的图自动带缩略图
npm run thumbs(tools/make-thumbs.mjs)是离线跑的,只覆盖当时已经在仓库里的图 ——
新上传的图要等谁手动跑一次才有缩略图,在那之前站点按原图显示。现在补上这一段:
- 上传前在图库里用同一套参数(720px / q68)再画一张缩略图,跟着主图一起传。
参数抽成
tools侧同一组常量(THUMB_WIDTH/THUMB_QUALITY),两边不许各写一份。 - Worker 写进
public/uploads/thumbs/<同名>.jpg,和主图同一次提交 —— 不会出现「图传上了但缩略图没传」的中间态。重名时主图会加时间戳后缀,缩略图跟着一起改。 - 缩略图是可选的:小图(< 400KB)、gif/svg、比 720 还小的图都不做, 没带或格式不对时只是忽略它,上传照样成功(和以前的行为一致)。
/admin/images多回一个hasThumb,图库里给还没有缩略图的老图加一个角标, 点开面板会说明「跑一次 npm run thumbs 就补上了」。
测试
tools/check-worker-admin.mjs加两组(6.95 / 6.97): 三条动作各记一条、标题/链接/IP 都对、删除那条没有链接、plog 键没有 TTL、 默认窗口只回最近的 N 条、口令校验;以及缩略图写进 thumbs/、字节一致、 与主图同一次提交、列表里hasThumb正确、没带缩略图时也能传、重名时缩略图跟着改名。- 假 KV 的
list()现在真的支持 prefix 了 —— 原来它一律返回空数组, 「全部历史」那条路根本测不到(和真的行为不一致的桩等于没测)。
2.17.0 · 2026-09-20
新增:日记页 /diary/(按年 → 月 → 列表翻看)
站主要一个只收录日记、需要口令、按年月翻看的入口。
- 入口:
yuuu.love/diary,和/private/一样不列入导航(1a); 页脚另有一个不显眼的链接(1c)—— 不想要删掉Footer.astro里那一行即可。 - 口令:复用私人角落那套(Worker 的
/hidden),同一个 sessionStorage 键 (yuuu-private),所以/private/、/admin/*任一登过,这边不用再输。 - 三个视图都在这一页里,位置记在 hash 上(
/diary/#2026/08): 年(从 2024 起,倒序)→ 月(固定 1–12 全部显示,带篇数)→ 该月列表(日期+标题,倒序)。 后退键能用、单级可收藏。 - 不做任何自动生成:列表里只有真实写过的;空月份显示「还没有」。
- 一天一篇:月份视图里点「✍ 写这一天」→ 出现该月日期格(已写过的标金色); 点已写过的日子 → 直接打开那一篇并提示「那天已经写过一篇」; 点空白日子 → 跳到手机写作页并预填分类=日记 + 该日期。 写作页那边也做了兜底:若同一天已有日记,直接打开已有的那篇。
为什么不做成三个静态页(重要)
dist/ 下的一切都是公开可下载的。 静态页会把日记标题写进公开 HTML,
口令就形同虚设 —— 跟之前那个泄漏是同一类问题。
所以公开的只有空壳 + 口令框,清单在验口令后由 Worker 返回。
为此加了 audit 8.7:拿全部文章的标题去 grep diary/ 与 private/ 的产物 HTML,
一个都不许命中(现在 18 个标题零泄漏)。
其它
- 新增
tools/check-diary.mjs(真浏览器测口令 → 年 → 月 → 列表 + 一天一篇), 并进check:all。现在一共 11 项。 - 手机写作页支持
?cat=<分类>&date=<日期>预填(原只有?edit=<slug>)。
2.16.0 · 2026-09-19
站主提了 5 件事,做了 4 件(第 2 件「日记 年-月-日 层级」站主觉得工作量太大,先不做)。
1. 明暗随时间自动切换(+ 开关)
- 主题多了
auto状态:localStorage['yuuu-theme']=auto|light|dark,默认 auto。 - 白天区间默认 7:00–19:00 亮,可在设置面板里改(存
localStorage['yuuu-day'],形如"7-19"), 并支持跨夜区间(如"20-6")。 - 设置面板「明暗」里一个开关 + 两个小时下拉,还显示「现在 21 点 → 暗色」; 顶栏那个按钮仍然能用,一点就变成手动钉死(开关会同步关掉)。
- 页面一直开着也能到点自动切:每分钟对一次表 + 切回标签页时对一次。
- 只影响站点前台,后台页面保持原来的浅色。
3. 图库操作队列(攒改动,一次提交)
- 改名 / 改分类 / 删除先在本地攒着(卡片上出现「改名」「改分类」角标、描边高亮), 底部显示「N 项待提交」+ 全部撤销 + 提交;点提交才一次性推到 GitHub。
- 新增 Worker 接口
/admin/images/commit:收到一批操作后用一次 Git 提交全部落地 (改名连文章引用一起改,仍然是原子的)。实测 3 项改动 = 1 条提交。 - 上传递归立刻提交,不进队列 —— 图片必须马上落到仓库,否则关掉页面就丢了。
- 队列非空时关页面/刷新会提醒;提交前先在本地查重名,不用等服务端拒绝。
4. 手机写作页的「+」按钮终于真的浮在视口上了
- 根因是 CSS 陷阱,不是没写
position: fixed:.screen的入场动画用了animation-fill-mode: both,元素上残留 transform → 它成了后代的包含块, 里面的 fixed 按钮就改成相对整个页面定位 → 被推到最底部。 实测:按钮视口位置 top=4336(页面高 4412、视口只有 844); 把 fill-mode 改成backwards之后变成 768(正确贴在视口右下角)。 - 图库和文章管理页有同样的问题(同一份 CSS 抄过去的),一起修了。
- 加了自检盯住这个陷阱(
.screen的动画不允许出现both)。
5. 文章页多了一对跳转按钮
- 前台文章页左下角(手机上右下角)两个圆按钮:↑ 开头 / ↓ 末尾。
- 位置取自 article 元素本身,所以是「文章的首尾」而不是整页首尾; 到了就自动置灰(避免点了没反应让人以为坏了)。
- 平滑滚动交给 CSS 的
scroll-behavior,在prefers-reduced-motion下自动变回瞬时跳转。
其它
- 首屏行内 JS 预算从 10KB 放宽到 12KB(站主拍板:「超了 10KB 没事,体验好就行了」)。 起因是 auto 主题必须在首屏绘制前算完,只能放 is:inline 脚本里。 检查保留,作用是防止以后失控膨胀。顺手把首屏脚本里的中文注释搬到 HTML 注释里 (一个汉字 3 字节,注释搬出来省了 1.2KB)—— 说明没丢,只是不计入 JS 预算。
- 新增两个真浏览器回归测试(进 check:all):
check-theme-jump.mjs(把时钟拨到各个小时验证明暗边界与跨夜区间、实测两个跳转按钮)、check-decap-extras.mjs。现在 check:all 一共 10 项。
2.15.0 · 2026-09-19
新增
- 全站页面索引:https://yuuu.love/admin/map/
页面多了容易忘,这一页把所有后台页、站点页、公开数据文件、Worker 接口、
仓库里的位置、常用命令列全,还带三条硬规矩与「加分类要改 5 处」的提醒。
- 入口放在 Decap 底部的工具条第一位(
/admin/页最下面), 另外图库 / 文章管理 / 手机写作页的登录页也各加了一个链接。 - 它是静态页(
public/admin/map/index.html),本身就是个清单,不含任何口令或令牌; 没有口令保护是为了随手能打开,所以别往里放敏感信息(这一点写在文件注释里了)。 - 「最后核对」那行写了当时的文章数与图片数,方便一眼看出有没有过期。
- 有一个自检会遍历页内链接,确认都指向真实存在的产物(避免索引自己写错地址)。
- 入口放在 Decap 底部的工具条第一位(
2.14.0 · 2026-09-19
新增
- 图片重命名(
/admin/g/和 Decap 后台的媒体库都有)。- 图库里点图片 →「重命名…」;Decap 媒体库里每张图上有个 ✎ 按钮(见下面的说明)。
- 只让改主名,后缀强制沿用原后缀(避免把 .jpg 改成 .png 后浏览器按错类型解析)。
- 默认连同文章引用一起改:图常常已经被文章引用了,只改文件名会让
立刻变破图。改名和改引用放在同一次提交里 —— 要么都成功,要么都没发生。界面上可以关掉(不勾「同时更新引用」)。 - 分类索引跟着改名走(索引是以文件名为键的);目标重名会明确报错,不覆盖。
- 看原图(大图)。
- 图库里「看原图(大图)」打开全屏灯箱(原来只是新标签打开)。
- Decap 媒体库里每张图上有个 🔍 按钮,同样是全屏灯箱 + 「在新标签打开原图」。
说明:Decap 那边的两个按钮是「运行时注入」
Decap 的媒体库没有给插件留扩展点:界面文案里只有上传/删除/下载/复制路径/搜索/选择,
没有重命名;DOM 里几乎没有 data-testid(只有 card-file-icon),类名都是
styled-components 的哈希;换成自定义媒体库要 CMS.registerMediaLibrary + React 组件,
而这个页面没有构建步骤。所以只能在运行时观察 DOM,往媒体卡片上挂两个小按钮
(public/admin/decap-extras.js)。两条自我约束:找不到元素就什么都不做、
只靠 img[src*="/uploads/"] 定位(不依赖任何哈希类名)。
新增 tools/check-decap-extras.mjs 做回归测试 —— 注入代码最容易出闭包问题
(「点第一张的按钮,弹出的却是最后一张」),这种错肉眼几乎看不出来。
这个测试就是用来钉住它的(确实当场抓到了一个 var 闭包 bug)。
内部整理
- 后台路由只登记一处:原来 fetch 里的路由表和
handleAdmin里的实现各写一遍, 加「重命名」时我只加了后者 → 线上一直 404(测试才发现)。 现在统一成ADMIN_POST_PATHS/GALLERY_PATHS两张表。
2.13.0 · 2026-09-19
修复
- ★ 图库上传的图,Decap 后台的「媒体」里看不到。
根因是 Decap 的硬限制,不是我们的 bug:它加载媒体用的是
getMedia(folder) { return this.api.listFiles(folder) }, 而listFiles默认depth = 1、且过滤掉路径里含/的条目 —— 只列media_folder根目录下的文件。 分类做成子目录之后(uploads/celeste/xxx.jpg),Decap 就永远看不到它们。- 改法:文件平铺在
public/uploads根目录(Decap 看得到), 分类改记在public/uploads/categories.json(图库照样按分类展示)。 两边都能看到全部图片。 - 附带的两个好处:改分类 = 改一行 JSON(不用搬文件,
也就不会再有「二进制被搬坏」那类事故);前台 Markdown 路径变得扁平好读
(
/uploads/0b91ecca….jpg,和 Decap 自己命名的风格一致)。 - 兼容迁移期:文件还躺在子目录里的老图,列表时按子目录名当分类;
一旦通过图库改了它的分类,就会顺手挪回根目录(这样 Decap 也能看到)。
已有的那张
celeste/xxx.jpg已经用 git 挪好并登记进索引。
- 改法:文件平铺在
- 批量删除漏清分类索引:删了文件但
categories.json里还留着那条记录 (指向空气的分类)。测试盯出来的,已修。
2.12.0 · 2026-09-19
变更(按用户要求)
- 去掉「正文至少 20 字」的要求。 短句、一张图配一行字、一句话日记都是正当内容。
原来这条规则写在四个地方(
tools/verify.mjs、Worker 的/admin/save、 手机页的validate()、以及字数提示),现在四处一起去掉。 测试也反过来盯着它:断言「19 字放行 / 一个字放行 / 空正文放行 / 只有图片放行」, 并且检查 verify 与 Worker 里都不再出现「正文长度 < N」的判断 —— 防止哪天被偷偷加回来。 标题仍然必须有(没有标题就生成不了 slug)。 - 手机写作页新增「回车即换行」(默认开,可在「更多设置」里关)。
Markdown 里单个换行不算换行,会被合并成一段 ——
用户报的「我自己换了行,上线后没有了」就是这个(他写的俳句三行被渲染成一行)。
- 为什么不做成全局的
breaks(GitHub 那种):站里 13 篇文章含多处软折行 (长句在源码里折成两三行),一开全局它们会在句子中间凭空多出换行。 所以改成写的时候才转换:保存时把单个换行写成 Markdown 硬换行(行尾两个空格), 只影响你保存的这篇,老文章一个字都不动。 - 围栏代码块里的换行原样保留(那是代码,加空格会改坏它);已经有硬换行的行不重复处理。
- 为什么不做成全局的
修复
- ★
/admin/p/登录循环(输完口令又回到登录页)。 根因:ui.js是type="module"(延迟执行),加载完成前登录表单还没有监听器。 用户在模块加载完之前输口令按回车,浏览器会原生提交(GET 带上?password=…)→ 页面刷新 → 又看到登录页。手机上网络慢一点就很容易撞上。 修法:在表单后加一小段内联脚本(立即执行)先拦掉默认提交;模块加载好后再接真正的逻辑。 已用无头浏览器验证过:把ui.js整个屏蔽掉(模拟模块未加载), 此时提交不会再刷新页面。 另外把「收到 401 就把人踢回登录页」改成留在当前页显示原因 + 一个「重新输口令」按钮 —— 万一服务端偶发拒绝,也不会表现成「刚验证完又回登录页」的循环。
2.11.0 · 2026-09-18
修复
-
★ 图库「图片丢失」—— 其实是图片被内容损坏了。 用户报「给图片新增分类后图片就没了」。查仓库里的字节:
字节数 开头 本地原图 55965 ff d8 ff e0(正常 JPEG)仓库里那张 104912 ef bf bd ef(UTF-8 替换字符 U+FFFD)原因:
/admin/image/move用ghGetFile(UTF-8 解码)+toBase64(重新编码)搬图片。 二进制里的非法字节全变成 U+FFFD(3 字节),体积涨了快一倍,浏览器也渲染不出来 —— 看起来就像「图没了」。 修法:新增ghGetBlob/ghPutBlob,图片全程走 base64,一步都不解码成文本。 -
测试的盲区一起补了:假 GitHub 之前把
content解码成文本再编码回去, 所以二进制损坏根本测不出来。现在桩和真 GitHub 行为一致(content永远是 base64), 并加了一条真实 PNG(含0x00/0xFF/0x89)走完整归类的断言、逐字节比对。 反向验证过:把坏实现放回去,测试立刻报efbfbd…vs89504e…,与线上现象一致。 -
★ 泄漏:后台搜索索引被生成到了公开目录。 我一开始把含全文的
posts-index.json生成到dist/private/—— 那是静态托管目录,任何人访问/private/posts-index.json都能拿到全部正文、 草稿和隐藏文章。「私人角落」那两份清单靠的是 Worker 的口令门槛,不是文件藏得深。 现在索引只存在 KV 里,由POST /admin/reindex从 GitHub 现场构建、读取得持 ticket。 另外悄悄话:/private/posts.json里那段 2KB 正文(其实是摘要)也去掉了。 审计新增第 8.55 节:公开目录里出现正文类文件即失败。
新增
- 图库批处理:多选(全选 / 清空)→ 批量改分类 / 批量删除。
Worker 走 Git tree 接口,N 张图 = 1 条 commit(不是 N 条或 2N 条),
用
base_tree保证没选中的文件不被误删;单批上限 60 张。 - 文章管理页
/admin/p/:全部文章(含草稿)+ 真搜索 + 分类筛选。 Decap 的view_filters只对单个字段做正则、且默认看不到草稿,所以另做一页。 搜索跨标题 / 分类 / 标签 / 摘要 / 正文并打分排序,列表里标出命中位置和正文片段。 - Decap 也加了 8 个筛选按钮(4 个分类精确匹配 + 其它分类 + 草稿/置顶/隐藏)。
config.yml里注明:pattern是当正则用的(new RegExp(pattern).test(值)), 所以^安利$这种写法有效。 - 手机写作页支持
/admin/m/?edit=<slug>:从文章管理页点一篇直接进编辑。 四个后台页面现在互相都有入口。
2.10.0 · 2026-09-18
新增
- 新分类「安利」(看完电影 / 番剧 / 书之后想推荐或吐槽的)。
改动落在四处:Decap 后台的下拉、手机写作页的分类胶囊、归档页的分类色、
以及「没指定封面时按分类自动挑」的配色(安利用玫红 340 + opera 样式)。
- 手机写作页现在能直接选分类(之前只能保留原分类,新建时固定随笔)。
check-mobile新增一条对账:后台下拉和手机页的分类集合必须一致 —— 漂移的后果很隐蔽:手机上选了「安利」,电脑用 Decap 打开时分类显示空,一保存就被抹掉。
- 图库
/admin/g/:图片分类管理(手机和电脑都能用)。- 分类 =
public/uploads下的子目录名。这样 Decap 自带的媒体库也能按文件夹浏览, Markdown 里的路径也自带分类(/uploads/表情包/xxx.jpg); 已经在根目录的老图显示成「未分类」,不用迁移。 - 网格浏览 + 分类筛选(带数量)+ 搜文件名/分类;点开一张图可以 复制路径(写文章时直接粘)、换分类、看原图、删除。
- 上传:相册多选 → 先选分类 → 逐张上传。上传前自动压缩: 最长边 1600、质量 0.85 转 JPEG(手机上随手拍的照片动辄 5MB,直接进 git 仓库又慢又臃肿); gif 和 svg 不压(压了动画就没了)。
- Worker 新增
/admin/images、/admin/image/upload、/admin/image/move、/admin/image/delete, 一样是 token 只在服务端,浏览器只拿 ticket。
- 分类 =
修复
- 正文的换行不再被吞。手机写作页保存时,正文只做一件事:CRLF 统一成 LF;
其它一律原样保留 —— 结尾的空行、行尾两个空格(Markdown 硬换行)、列表缩进都不动。
check:worker新增 6 种换行场景的断言,并且逐字符比对提交内容与落盘内容。 - 图片列表永远是空的:
IMAGE_EXT我写成了/^(jpe?g|png|…)$/(要求整串等于扩展名), 于是列图片时传完整路径全被判非法,而上传时传的是裸后缀所以碰巧正常 —— 表现成「能上传但列表没图」,很难往正则上想。改成匹配结尾。
2.9.0 · 2026-09-18
新增
- 手机写作页
/admin/m/—— 专为手机做的极简写作界面,取代拿 Decap 后台硬撑手机的做法。 为什么另起一页:Decap 的界面按桌面宽度写死(min-width:800px+flexDirection:column的 SplitPane + 百分比高度),手机浏览器只能把整个界面缩到 49% 塞进屏幕 —— 字极小、说明文字被 压成竖条。我先后试了两版 CSS 覆盖,都把「能用的缩放版」改成了「真的坏」,见 2.8.2。 结论是:别人的桌面 UI 不要硬改成手机 UI,另做一个。- 界面:登录(和私人角落同一个口令)→ 文章列表(搜索 / 已发布 / 草稿 / 隐藏 四档筛选) → 写作(标题 + 正文 + 标签 + 可折叠的更多设置)→ 保存。
- 移动端该有的都有:字号 >= 16px(否则 iOS 聚焦会放大整页)、
dvh高度、scroll-padding-bottom(键盘弹起时输入框滚进可视区而不是整页被推走)、 安全区env(safe-area-inset-*)、触控目标 >= 44px、prefers-reduced-motion时关掉全部动效。 - 保存按钮只在有未保存改动时才浮出,平时界面保持安静;字数实时显示,不足 20 字会变金色提醒。
- Worker 新增四个接口(
/admin/posts/admin/file/admin/save/admin/delete):- GitHub token 只在 Worker 里,浏览器只拿 ticket —— 就是口令通过后发的随机串
(KV 里
ticket:<uuid>,6 小时过期)。前端文件里出现 token 会被verify拦下。 - 保存时服务器自己写
updated;改已有文章只替换需要变的 frontmatter 行, 注释和字段顺序原样保留(不是整份重写);新建才拼一份完整 frontmatter。 - 路径白名单:只允许
src/content/posts/下的.md,且不许出现..。 - 正文长度校验与
tools/verify.mjs同一条规则(trim().length >= 20,标点也算)—— 一开始我多写了「去掉标点再数」,会导致手机说能存、构建时被 verify 拦。
- GitHub token 只在 Worker 里,浏览器只拿 ticket —— 就是口令通过后发的随机串
(KV 里
- 构建时多生成一份
dist/private/posts-all.json(含草稿的全部文章清单),供手机写作页列清单; 正文仍然只放元信息,点开哪篇现取哪篇。
验收(新增两个检查,都接进了 check:all)
npm run check:worker(tools/check-worker-admin.mjs):拦截 GitHub API, 直接调用 Worker 的处理函数,检查它实际提交了什么 —— frontmatter 是否保留注释与未改字段、 发布日期是否被误改、标题里的冒号井号是否被正确引号包住、新建路径是否合理、 路径穿越/空标题/太短正文是否被拦、没配 token 时提示是否点名了缺哪个 secret。 这个过程逮到两个真 bug:新建文章被文件名校验误拦(rel为空时正则必然不匹配)、 以及上面那条长度规则不一致。npm run check:mobile(tools/check-mobile-app.mjs):写作页的纯逻辑(校验、标签解析、 列表文案、相对时间)加上页面结构自查 —— 其中一条很实用:ui.js里取的所有id必须在index.html里存在(对不上就是「点了没反应」)。- 另外用无头 Edge 把整页跑通了:登录 → 列表 4 篇 → 搜索/筛选 → 打开编辑 → 短正文被拦 → 保存成功 → 新建保存,并逐个界面截图看过排版(无横向溢出、字号达标、按钮不重叠)。
2.8.2 · 2026-09-18
修复
- 手机后台进文章只剩一片白板(PC 正常)—— 是我上一版加的手机 CSS 干的。
复盘:我在
@media (max-width:759px)里加了[class*="SplitPane"] { display: block }和[class*="Pane"] { height: calc(100dvh - 66px) }。 但 Decap 的编辑器是 SplitPane:flexDirection: column+ 绝对定位 + 百分比高度, 高度靠100%一层层算下来 —— 把栏位高度钉死、又把 flex 改成 block,分栏直接塌掉。 同一版还改了overflow,把滚动链截断,键盘行为反而更糟。- 现在布局和高度一概不碰,只保留「拆掉硬编码的
min-width:800px」这种 约束性质的覆盖,以及颜色(html底色)和scroll-padding/scroll-margin(只影响聚焦时滚到哪里,不参与布局)。 - 教训写进文件注释和 HANDOFF 9.9:别人的组件库,别替它算布局。
- 现在布局和高度一概不碰,只保留「拆掉硬编码的
- 预览里的封面是一块空白。原因不止一个,都补上了:
- 加载失败会退回渐变 + 标题首字(原来图挂掉就只剩空白,还没提示);
- 没选封面时,渐变上会写明「没选封面 · 按封面色相画渐变(hue 202)」, 不留一块说不清的白;
- 色相为空 / 非法时退回站点默认的 202,不会拼出无效的 CSS。
2.8.1 · 2026-09-18
修复
- 「最近修改」和「最新发布」又变成同一个顺序了(用户报「三种切换没区别」复发)。
根因是清单生成脚本的兜底:三篇隐藏文章 frontmatter 里都没有
updated, 于是「最近修改」退回用git log -1 --format=%cs,而%cs只精确到「天」 —— 同一天被碰过的几篇日期全一样,排出来自然和按发布日期排一模一样。- 改成取
%cI(精确到秒的时刻),同一天的不同提交也能分开。 - 再加一个不依赖 git 的次要依据
touches(改过该文件的提交数): 同一批提交碰过的文件时刻也会完全相同,这时候靠它分先后。 为什么不用「文件修改时间」:构建环境的 clone 会把所有文件的 mtime 刷成同一时刻,同样没用。
- 改成取
- 上一轮我是用「日期互不相同」的数据验的,所以没发现这个洞 —— 这次把验收写进
npm run audit:第 8.5 节拿真实清单跑三种排序,两两相同就判失败, 顺带检查清单里每条都带touches、以及排序只重排不丢条目不重复。 - 排序规则抽到
src/lib/sort-posts.ts,让页面和审计共用同一份实现 —— 之前逻辑写在.astro的is:inline脚本里,检查脚本只能照抄, 抄完就开始各自漂移。(页面里仍有一份等价实现,因为is:inline不能 import; 文件里注明了改规则要两边一起改,审计会兜住不一致。)
2.8.0 · 2026-09-18
新增
- 后台预览长得像线上文章页了(
public/admin/preview.js+preview.css)。 原来点「打开/关闭预览」看到的是 Decap 默认的裸排版:只有正文,没有标题层级、 没有日期/分类/标签那一行、没有封面 —— 和线上差太远,等于没法定版式。 现在按PostLayout.astro的结构重画:衬线标题、日期/「改于」/分类/字数/阅读时长、 标签、封面(封面池 id 会解析成真实图片,没选封面就用封面色相画渐变)、正文排版 (标题、引用、代码块、列表、表格、行内代码)。草稿也能预览。 用 iframe 隔离(decoupled: true),这些样式碰不到后台自己的界面。- 样式没去引构建产物:
dist里的 CSS 文件名带哈希(BaseLayout.DpRaR-Rf.css), 每次构建都变,后台里写死必然某天 404。令牌是手抄的,改主题时要一起对 (文件头有说明)。 - 它不是线上页面的镜像。刚保存的文章要等 Cloudflare 构建(约 1 分钟), 想看真实的就点编辑器右上角「查看发布」。这句话也写在预览面板顶部。
- 兜底 Markdown 渲染器是手写的(不引任何第三方库):段落、1~4 级标题、引用、
代码块、列表、分隔线、粗斜体、行内代码、链接。验收标准是「一字不丢」 ——
npm run check:preview拿最长的一篇真文章比对纯文本, 抽查 13 个 8 字片段必须全命中,另测空正文 / 未闭合代码块 / 5000 字无换行等边界。
- 样式没去引构建产物:
修复
- 手机后台:整页被撑到 800px 宽、右侧一片空白、底部还有白条。
用无头 Edge + CDP 量出来的实据(390×844):
visualViewport是 390, 但innerWidth被撑到 800,documentElement.scrollWidth也是 800。 原因:Decap 桌面优先,AppMainContainer/ToolbarContainer/EditorContainer三处都硬编码了min-width:800px;而且编辑器在宽屏是「写作 / 预览」左右分栏, 手机上两栏各 400px 根本塞不下。现在窄屏(<760px): 拆掉最小宽度、只留写作栏(预览用工具栏按钮随时切)、宽度锁 100%。- 底部的白条:Decap 把底色画在
body上,html是透明的, 露出来的是浏览器画布自己的颜色(通常白)。地址栏收放、键盘弹起都会露出来。 现在html一起刷成同色。 - 高度原来靠
height:100%+ 写死的padding-top:66px凑, 键盘弹起、地址栏收放都会错位。窄屏改用100dvh(动态视口高度)并按.Pane补差量。
- 底部的白条:Decap 把底色画在
- 手机后台:点输入框整页上移、指针跑到屏幕外,看不到在打什么。
这个是同一批原因的结果:界面比屏幕高、
EditorContainer又是overflow:hidden, 浏览器的「把获得焦点的输入框滚进可视区」失败,于是整页被推走。 现在给滚动容器加scroll-padding-bottom: 45dvh(键盘之上的可视区才算数)、scroll-margin-bottom,并把overflow从hidden放开成可滚 —— 滚动链不通, 浏览器就没法把输入框滚进来。 viewport补上viewport-fit=cover,刘海屏/手势条上给工具栏留安全区内缩。
新增(工具)
npm run check:preview(tools/check-admin-preview.mjs):后台预览模板的回归检查。 已接进check:all。- 后台的移动端问题是用无头 Edge + CDP 真跑一遍定位的:
用假 token(
localStorage['decap-cms-user'],必须带backendName: 'github')+ 拦截api.github.com(/user、/repos/...、/git/trees/...、/contents/...、/graphql), 就能在本地进真实编辑器、量真实几何、截图,不用真的登录 GitHub。 探针要求backend里不能带base_url(带了会走 OAuth 跳转,不走本地 token)。
2.7.0 · 2026-09-18
新增
- 私人角落(
/private/):口令在 Worker 里校验,口令不对连文章清单一根毛都拿不到 (不是前端比对字符串那种一看源码就穿的形式)。通过后展示被隐藏的文章, 支持全文搜索与三种排序;进入 / 离开都记日志(IP、设备、时间、停留秒数),/admin/logs/可看、可导出 CSV、?all=1翻全部历史。 日志在 KV 里分两层:热数据log:<id>(留 90 天)+ 永久归档day:YYYY-MM-DD(不设 TTL), 所以 90 天到期只影响按条查看,历史统计不会跟着消失。 同一 IP 连错 5 次锁 15 分钟(fail:<ip>计数)。说清楚边界:这是防君子。站点是纯静态的,文章本体没有加密, 知道真实 URL 的人仍然看得到,写秘密请另想办法。MAC 地址浏览器根本不给,做不了。
- 排序下拉(
src/components/SortSelect.astro):主页与私人角落共用。 没用原生<select>(它的展开列表由浏览器画,圆角 / hover 都改不了), 按钮和列表自己画,真正的<select>留在 DOM 里只存值 —— 表单语义和可达性都保住, 页面代码照旧getElementById + change就行。三种排序:最新发布 / 最早发布 / 最近修改。 - 卡片风格 × 皮肤:卡片 4 种(玻璃 / 描边 / 纸质 / 浮起),皮肤 7 套 (枫丹 / 歌剧院 / 深海 / 薄荷 / 琥珀 / 翡翠 / 靛蓝),设置面板里带缩略图。
- 置顶:
pinned: true的文章排在最前并显示图钉与左侧金线。 - 日记类(
category: 日记)默认隐藏,被隐藏的文章也能带着?from=search跳进私人角落。
修复
- 「最近修改」排序看起来没反应 —— 其实是真的没生效。
排序的下拉箭头、选项高亮、写回隐藏
<select>、派发change都由 SortSelect 组件负责, 重排列表则由页面脚本负责,两半之间的那根线在人手里。私人角落只写了前一半:change事件发出去没人接,于是选中项变了、列表纹丝不动。 (上一轮只验证了「组件会派发 change」,没验证「页面接住了」—— 验证做了一半。) 以及const again = ...那行被一次编辑压进了上一行的注释里,一直靠浏览器的 「带 id 的元素自动成为全局变量」兜着,属于埋着的雷,一并修掉。 - 新增
npm run check:sort(tools/check-sort-link.mjs)盯死这类断线: 把产物里真实的两段脚本一起放进假 DOM,点每个选项并断言 「值写回 → 列表确实重排 → 三种顺序两两不同」。 已反向验证过:把change监听拆掉它立刻失败(exit 1),不是个只会点头的测试。 已接进npm run check:all。 npm run check:all不负责构建(它验的是已有的dist), 改完源码要单独跑npm run build—— 这次差点被它骗过去(源码改好了、dist还是旧的, 于是测试「依然失败」,看着像修错了地方)。
2.6.0 · 2026-09-16
新增
- 隐藏文章(日记不想被看见)。判定两条,满足其一即可:
category: 日记(自动,日常用这个)或private: true(手动,给非日记的文章)。 生效范围:首页 / 归档 / 标签云 / 标签页 / 分类筛选 / Pagefind 搜索 / RSS / sitemap / 上一篇下一篇导航 / 搜索引擎收录 —— 全都不出现; 但直接开链接仍然能看(纯静态站没有登录,这是能做到的极限,写秘密请另想办法)。- 规则只有一处实现:
src/lib/hidden.ts。渲染期和构建配置期共用它 —— 后者不能 importsrc/lib/posts.ts(那会拖进astro:content这个渲染期才有的 虚拟模块,构建配置阶段直接报 “Cannot find module ‘astro:content’”)。 - 分享卡片(og:description)对隐藏文章换成中性摘要,免得链接被贴出去时摘要外泄。
- 后台加了「隐藏」开关,并说明「选日记分类会自动隐藏」。
npm run audit新增第 8 节:逐项验收上述每个出口,泄漏即失败。
- 规则只有一处实现:
修复(这一节里有两个「本地能跑、线上必炸」的坑)
-
幽灵依赖又咬了一次:
astro.config.mjs里import 'yaml'借用了 astro 的传递依赖 —— npm 会把它提升到顶层所以本地构建正常,Cloudflare 用 pnpm 的隔离模式时 直接报Cannot find module 'yaml'、整个站点发不出去。 现在构建配置里只用极小的正则取需要的两个键;tools/verify.mjs里的同类借用改成把yaml显式写进 devDependencies。 -
同样是这一类:
astro.config.mjs一开始想直接import { getPosts } from 'src/lib/posts', 而那个模块依赖astro:content(渲染期的虚拟模块),构建配置阶段加载会失败 —— 所以判定规则抽到了独立的src/lib/hidden.ts,两边共用。 -
可选日期允许空值。网页后台清空「最后修改」时会写
updated: '',z.coerce.date()把它转成 Invalid Date 对象,Astro 报InvalidContentEntryDataError、整个站点构建失败。 -
tools/verify.mjs两处对齐:schema 同样处理空值; frontmatter 改用真正的 YAML 解析器(原来手写的那版只认tags: [a, b], 遇到后台保存出来的多行列表就误报 schema 错)。 -
audit-dist.mjs的「加载更多」检查改为按公开文章总数判断 —— 公开文章不足一页时本来就没有下一页,缺api/posts/*.json是正确的。
2.5.1 · 2026-09-16
新增
- 写作流程脚本化,三条命令:
npm run new—— 逐条问(标题 / 日期 / 分类 / 标签 / 封面样式 / 摘要), 直接回车用默认值;也支持npm run new -- "标题" 技术 "前端, 笔记"一条命令建好。 生成的草稿带draft: true,构建时会跳过,先写草稿是安全的。npm run posts—— 在资源管理器里打开文章目录。npm run publish—— 自检 → 构建 → 提交 → 推送一条龙。 构建不过就什么都不提交(不会把错误推上去让 Cloudflare 报错); 有草稿会列出来提醒;提交说明写进临时文件再git commit -F, 绕开 cmd.exe 下git commit -m "中文"乱码的问题。
- README 补「写文章」完整章节:三种写法、改文章、本地预览、什么才算发布, 以及为什么必须 push 才会更新线上(线上是 Cloudflare 在构建 GitHub 仓库)。
修复
- 主题切换的太阳 / 月亮图标没有居中。原来用的是
☾/☀两个 Unicode 字符 —— 它们的墨迹在字体 em 框里的位置随系统字体变,靠 CSS 居中在有些机器上会偏上或偏下。 改成内联 SVG(viewBox收紧到图形包围盒),居中由几何决定,不看字体脸色; 旋转切换动画保持不变。已用 Edge 无头截图逐个核对深色 / 浅色两种状态。
2.5.0 · 2026-09-16
新增
npm run smoke(tools/smoke-search.mjs):搜索的运行时冒烟测试。verify/audit只能证明「引擎没进首屏、chunk 切出来了」, 证明不了「用户聚焦之后它真能跑」。这个脚本用一个极小的 DOM 假件 把dist里真实的按需 chunk 跑一遍:init 幂等、补跑第一次输入、 渲染结果、↑↓ / Enter / Esc / Ctrl+K /「/」/ 清空、索引缺失时的诊断分支。 改搜索引擎之前先跑它。另加npm run check= verify + smoke + audit。
性能
- 搜索引擎改成按需加载(
src/scripts/search-engine.ts)。SearchBox.astro里只留一个引导:聚焦输入框 / Ctrl+K /「/」 时才import()引擎本体。首屏要下的 JS 从 16.37 KB 降到 9.99 KB(gzip 后 实际传输),第一次达标 PRD 的 10 KB 红线 —— 其中行内 7.88 KB、外链 gzip 2.11 KB。 引擎自己的 chunk 是 7.80 KB(gzip 3.33 KB),用户一动手才下载。 - 引导分两段:快捷键与「正在准备搜索…」占位现在就装好,
保证 Ctrl+K /「/」第一次按下就有反应;引擎初始化要一点时间,
那之前敲进去的字由引擎
init()里的 240ms 补跑接住。 - 引擎 chunk 下载失败(离线 / 404)会在面板里明说,不会静默留个「正在准备搜索…」。
修复
- 补跑第一次输入的逻辑原来写成
if (input.value.trim()),而init()执行时 输入框必然是空的 —— 这个分支从来没生效过。现在无条件挂 240ms 兜底, 触发时再判断(已经有结果或已清空就跳过)。冒烟测试专门盯这条。 tools/verify.mjs增加「按需模块」检查:搜索引擎 / 设置面板只允许被import()引用,一旦有人改回静态 import 会立刻报错。tools/audit-dist.mjs增加产物层回归:首屏脚本里出现excerptLength/search-hit/pagefind.js等引擎特征串即判失败, 并核对引导里的import()确实指向那个按需 chunk。
2.4.0 · 2026-09-16
新增
- 搜索查询语法。支持
in:title,tag、tag:前端、category:技术、date:2024-01..2024-12、words:500..2000、min:1..5,以及"精确短语"。 文章页的sr-only区块里补齐了data-pagefind-filter/data-pagefind-meta, 筛选走 Pagefind 原生筛选器,区间比较在客户端做。搜索时面板顶部会显示生效的条件。注:需求里提到的
size:1024..4089是文件管理器的语义,博客文章没有「文件大小」, 已改用语义等价的words:(字数)与min:(阅读分钟)。 - 皮肤系统。
theme.css改成「主色只存 RGB 分量--a/--g,其余全部派生」, 皮肤只需覆盖这两行,线条 / 悬停 / 渐变 / 极光背景一起跟随。 三套皮肤:枫丹(水蓝)、歌剧院(玫瑰)、深海(幽紫),深浅各一份压暗版。 主题面板可同时切模式与皮肤。 --accent-solid/--gold-solid用@property注册成<color>, 这样渐变里的色标也能平滑过渡,而不是硬切。
修复 / 调整
- 搜索聚焦聚光:整页模糊变暗,搜索框高亮,导航淡出。
遮罩是
pointer-events: none(PRD 硬性要求,旧版漏了会导致全站点不动)。 - 搜索结果只展示一次:点别处关掉后会连结果一起清空,再次聚焦不会又冒出来。
- 首屏禁止选中(
.no-select),正文不受影响。 - 顶部导航精简为 首页 / 关于 / 更新,归档与标签交给页脚(页脚本来就有)。
- 搜索框光标与输入内容始终居中。
2.3.1 · 2026-09-16
新增
-
封面池(
src/lib/covers.ts)。文章 frontmatter 的cover现在支持三种写法:写法 含义 cover: stand用封面池里的(推荐) cover: /covers/my.jpg用你自己的图( public下的绝对路径)cover: https://…外链 不写则按 slug 从池子里稳定挑一张 —— 同一篇文章每次构建拿到的封面一致, 不会刷一次换一张。12 篇示例文章已全部改成池子 id。
调整
- 主页首屏占满一屏(
calc(100svh - 68px)),默认看不到文章列表。 - 首屏只留徽记 +
FONTAINE · FURINA+yuuu+ 一行统计,砍掉了原来的长句和大标题。 - 徽记放大到
clamp(148px, 22vw, 244px),站名放大到clamp(54px, 12vw, 124px)。 - 下滑时首屏整体缩小 20% 并淡出(只动 transform / opacity),文章版块顺势接上来。
- 更新历史里内容本来就装得下的版本,不再显示「展开」按钮 —— 点了没新内容很迷惑。
改成按
scrollHeight实测判断。
移除
- 海浪背景。它原本对齐首屏底边,首屏改成满屏后会横在正文中间,
滚动时又赖着不走。已连同
Background的waves属性和 CSS 一起删除。
修复
- 搜索框光标默认停在中央(
:placeholder-shown时text-align: center), 一开始打字就回到左对齐,避免边打边跳。
2.3.0 · 2026-09-16
新增
- 真的 favicon。用
tools/make-icons.mjs处理参考图:从四条边泛洪填充抠背景 (不是「白色变透明」—— 角色头发也是白的,那样会打出窟窿),裁边, 输出favicon.ico(16/32/48 三尺寸)、icon-192/512.png、apple-touch-icon.png和一份site.webmanifest。 - 7 张表情包处理成透明背景的 WebP,放在
public/mascot/,用作文章封面与空状态插画。 - 主页重构成两屏:首屏只有徽记、站名、一句话和统计;文章列表整体下沉 + 模糊,滚进视口后一次性浮现。下滑提示带轻微浮动动画。
- 搜索快捷键
/(Ctrl/Cmd+K保留)。↑↓ 在结果间移动,Enter 打开选中项。 - 从搜索结果点进文章会定位:链接带上
?q=,文章页找到正文第一处匹配, 包上<mark>并scrollIntoView({ block: 'center' })滚到屏幕中央, 配一个只动 opacity/transform 的脉冲圈。 - 更新历史按版本折叠:默认只展开最新一版,其余收成 170px 并底部渐隐, 每段有「展开/收起」,顶部有「全部展开」。
tools/set-domain.mjs+pnpm run domain:一条命令改全站 6 处域名。
修复
- 首页分类筛选点任何分类都是 0 篇。
index.astro读el.dataset.category, 但PostCard.astro从来没输出过data-category,永远是undefined, 所有卡片都被hidden。已补上属性。 - 搜索框光标被 placeholder 遮住。快捷键提示原本写在 placeholder 里,
聚焦后文字紧贴光标。现在提示做成独立的
<kbd>徽标,聚焦后淡出, placeholder 在聚焦时也变透明。 - 卡片 hover 生硬。原来 300ms 线性位移 + 阴影重绘。现在统一用
cubic-bezier(0.22, 1, 0.36, 1)长缓动,阴影改由伪元素淡入 (不逐帧重绘box-shadow),封面缓慢放大,标题变色。 sharp之前是幽灵依赖(和zod一样的坑),已显式声明。
与 PRD 的偏离
- PRD 9.5 禁止用
filter: blur()做动画,但需求明确要求「文章下沉模糊」。 采取折中:模糊只在进入视口时过渡一次,transitionend后加.is-done把filter摘掉,不留长期合成层。tools/verify.mjs的对应规则也拆成两条:@keyframes里动 blur 一律禁止;transition里含 filter 则要求必须配.is-done摘除。
2.1.2 · 2026-09-16
修复(搜索质量)
-
搜索结果被标签页污染。构建时 Pagefind 会提示
Did not find a data-pagefind-body element on the site. ↳ Indexing all <body> elements—— 意思是它把整页都索引了,包括导航栏、页脚和所有标签页。实测后果(用
pagefind.js在 Node 里跑真实查询):查询 修复前 修复后 芙宁娜 1 条,是标签页 /tags/芙宁娜/3 条,全是文章 玻璃 6 条,第 1 条是 /tags/设计/1 条,正是那篇文章 雨天 6 条(夹着无关标签页) 1 条 Astro 34 条噪音 4 条,全是文章 修法:给
PostLayout.astro的<article>加data-pagefind-body, 给底部按钮行加data-pagefind-ignore,只索引正文。 索引页数从 43 降到 12(只剩文章),这才是博客搜索该有的样子。 -
搜索结果能显示日期了。给日期元素加
data-pagefind-meta="date", SearchBox 里的meta.date从undefined变成了2024 年 5 月 20 日。
增强
npm run audit新增两项检查:文章是否都标了data-pagefind-body、 是否都带data-pagefind-meta="date"。这类问题不看产物是发现不了的。
2.1.1 · 2026-09-16
修复
- 幽灵依赖。
tools/verify.mjs用了zod与@astrojs/compiler,但没在 package.json 里声明。 npm 的扁平化把它们提升到顶层所以能跑;pnpm 的隔离式 node_modules 会正确拒绝, 报ERR_MODULE_NOT_FOUND。现已显式声明为 devDependencies, 并把 verify.mjs 的导入改成动态 import —— 缺依赖时给一句人话而不是甩堆栈。
新增
npm run audit(tools/audit-dist.mjs):对真实构建产物做验收, 而不是像 verify 那样只看源码。检查首屏 JS 实测字节、首页卡片数、 加载更多的静态 JSON、Pagefind 索引完整性、RSS/Sitemap、 以及[hidden]兜底和pointer-events保护有没有出现在产物 CSS 里。- 首屏内联 JS 的构成明细(会打印每段脚本的体积与用途),让性能预算可执行。
补充说明
build是astro build && pagefind --site dist。astro build 若以非 0 退出码结束, pagefind 不会执行,搜索索引会静默缺失。构建后请跑npm run audit确认。- ⚠️ Windows 的 cmd.exe 里不要把
#注释写在命令同一行。#在 bash 是注释、 在 cmd 里不是,整行复制会让后面的命令收到多余参数 (例如npm run build # 说明会让 pagefind 报unexpected argument '#' found)。 仓库里所有代码块都已去掉行内注释,可以整段复制。
实测数据(npm run audit,Astro 7.3.2 构建产物)
| 项目 | 结果 |
|---|---|
| 页面 | 44 个 HTML(12 篇正文 + 25 个标签页 + 归档/关于/更新/标签云/404) |
| 首屏 JS | 8.70 KB(行内 7.61 KB + 外部包 gzip 1.09 KB),红线 10 KB |
| 第三方脚本 | 公开页面 0 个 |
| 首页首屏 | 正好 10 张卡片,追加走 api/posts/2.json |
| Pagefind | 43 页 / 1694 词 / 57 文件 / 643 KB |
| RSS | 12 篇 |
| Sitemap | 42 个 URL |
2.1.0 · 2026-09-15
变更
- Astro 5.18.2 → 7.3.2。这是与 PRD「锁死 Astro 5.x」的一处有意偏离:
npm audit对 Astro ≤ 7.2.7 报出 1 critical / 1 high / 1 low, 逐条核对后确认相关代码路径本项目都没用到,但修复成本只有一行,且已实测兼容。 源码一行未改,tools/verify.mjs在 5 和 7 上都是全绿。 - 包管理器配置补齐:
pnpm-workspace.yaml放行esbuild/sharp的构建脚本。 pnpm 10+ 默认拦截构建脚本并以 exit 1 结束,会导致 Cloudflare Pages 安装步骤失败。 注意 pnpm 11 已不再读取package.json里的pnpm字段,配置必须放这里。 - 同时提供
package-lock.json与pnpm-lock.yaml,二选一保留。
⚠️ 破坏性要求
| 项目 | 旧 | 新 |
|---|---|---|
| Node | >= 18 | >= 22.12.0 |
| npm | — | >= 9.6.5 |
| pnpm | — | >= 7.1.0 |
Cloudflare Pages 的 NODE_VERSION 必须从 20 改成 22,否则构建直接失败。
2.0.0 · 2026-09-15
用 Astro 从零重构。这是一次推倒重来,不是迭代。
技术栈(全部锁死)
| 层 | 选型 |
|---|---|
| 框架 | Astro(静态输出,默认零 JS) |
| 内容 | Astro Content Collections(frontmatter 强校验) |
| 样式 | Tailwind CSS 4(主题变量走 CSS custom properties) |
| 动画 | View Transitions API + 原生 CSS |
| 搜索 | Pagefind(构建时生成静态索引) |
| RSS / Sitemap | @astrojs/rss / @astrojs/sitemap |
| 部署 | Cloudflare Pages |
| 写作 | Sveltia CMS + GitHub OAuth Worker |
新增
- 组件化:Header / Footer / PostCard / TagCloud / SearchBox / ThemeToggle / Background / CoverStyle,改一处全站生效。
- 内容层交给框架:不再手写 Markdown 解析器,表格、引用、代码块、任务列表全部由 Astro 处理,frontmatter 有 schema 校验。
- 文件路由:首页、
/posts/[slug]、/archive、/tags/[tag]、/about、/changelog、/rss.xml、/404。 - Pagefind 全文搜索:索引分片、按需加载,聚焦输入框才开始下载,不占首屏。
- 原生 View Transitions:水波淡入,零 JavaScript。
- 深色 / 浅色双调色板:460ms 整体联动过渡。
- 9 种封面样式:潮汐 / 星云 / 王冠 / 歌剧院 / 极光 / 星海 / 气泡 / 方格 / 自定义图片,全部是 CSS 渐变,零图片请求。
- 首页分页:首屏只渲染 10 篇,其余由构建时生成的静态 JSON 按需追加。
- Sveltia CMS:
/admin登录 GitHub 直接写文章,push 后 Cloudflare Pages 自动重建。 - OAuth Worker:
workers/oauth,处理 GitHub 授权的/auth与/callback。
砍掉
- ❌ 音乐播放器(与流畅度冲突)
- ❌ 空闲提醒(鸡肋)
- ❌ 6 种页面切换动画 → 只留 1 种「水波淡入」
- ❌ 装饰性水母 SVG
- ❌ 阅读次数 / 上次阅读时间(需要后端,第一版砍掉)
- ❌ 拼音搜索(Pagefind 中文分词够用)
- ❌ 评论系统(以后再说)
性能约定(硬红线)
- 首屏 JS < 10KB —— 实测约 1KB(Astro 默认零 JS,交互脚本全部内联)
- 只动
transform和opacity,不碰filter/box-shadow/width - 第三方脚本默认 0 个(统计脚本需手动填 token 才加载)
- 任何功能如果让首屏 JS 超标,砍掉重做
从旧版踩坑里继承的禁令
- 禁止用
display覆盖[hidden]的默认行为(全局已加[hidden] { display: none !important; }) - 禁止给全屏固定层不加
pointer-events: none - 禁止手写 Markdown 解析器
- 禁止全量加载所有文章到一个 JS 文件
- 禁止用
filter: blur()做动画 - 禁止用
localStorage存文章正文
1.4.1 · 2024-09-15(旧版)
- 修复全站按钮点不动:空闲遮罩用
display覆盖了[hidden]。
1.0.0 · 2024-09-12(旧版)
- 手写静态站点首次上线。
