130 Commits
Author SHA1 Message Date
wait4xandGLM-5.3 6b2e4d8666 feat(opensource): 热力图五方案动效预览上线
用户要求「热力图可以有更多的动效,而不是静态的一张图」,先上线五案供选型:
A 入场波纹 / B 光标光晕 / C 年度回放 / D 连续电流 / E 悬停周详情。
均守灰阶红线、reduced-motion 静态化、零第三方依赖。

验证: 构建 exit 0;content-lint 52 页 0/0/0;Playwright 逐案实测动效均生效
      (A hmRise 0.42s、B 光晕读数、C 第 10 周回放、D 12 格 hmFlow、E 整列 lift)

Spec: none (预览页需先上线供用户选择)

Co-authored-by: GLM-5.3
2026-11-04 20:09:54 +08:00
wait4x 545c91a0ff Update 文章 “writing-template” 2026-11-03 06:44:46 +00:00
wait4x 7149541a92 Update 文章 “mian-sha” 2026-11-03 06:42:54 +00:00
wait4x a868bf94fd Update 文章 “shu-zui” +1 2026-11-03 06:42:01 +00:00
wait4x a46430b088 Update 文章 “ni-tan” +1 2026-11-03 06:40:43 +00:00
wait4x 1ce9cd65ea Update 文章 “mian-sha” +1 2026-11-03 06:39:33 +00:00
wait4x 6e4d6c2f88 Update 页面文案 “opensource” 2026-11-03 06:38:05 +00:00
wait4x 6abca9f412 Merge branch 'spec/2026-11-02_blog-5i-fonts-polish'
Phase 5i 交付:字体切换失效根治(按内容诉求注入字体 CSS,此前四份字体 CSS 被
关在阅读轨分支里,技术轨与全站页面从不加载)+ 六款自托管 OFL 字族(@fontsource
切片粒度不达标→改原始 TTF 字频序细切,单页增量 27–46KB 全过闸门)+ 评论失败提示
误报的状态机修复 + 瞬间评论区配色同步(Artalk .artalk 类的 CDN 样式平级压制)+
灯箱右上关闭被全高侧栏遮挡修复 + 灯箱 caption 支持字体与颜色 + 取消彩色强调次数
限制 + 开源页项目墙恢复居中 + README 动效自托管与数据板 + 书架定稿方案 C(抽书
阅读、置顶图标化、多本置顶默认展开)。

Spec: 2026-11-02_blog-5i-fonts-polish
2026-11-03 12:47:54 +08:00
wait4x c9fb46f5a9 Merge remote-tracking branch 'origin/main' into spec/2026-11-02_blog-5i-fonts-polish
# Conflicts:
#	docs/管理台.md
2026-11-03 12:47:53 +08:00
wait4xandGLM-5.3 edc34a8a8c chore(spec): 归档 blog-5i——九任务收敛
Spec: 2026-11-02_blog-5i-fonts-polish

Co-authored-by: GLM-5.3
2026-11-03 12:47:13 +08:00
wait4xandGLM-5.3 83e0b863fc chore(spec): 5i 勾选 T7/T9(九任务全勾)
Spec: 2026-11-02_blog-5i-fonts-polish

Co-authored-by: GLM-5.3
2026-11-03 12:35:20 +08:00
wait4xandGLM-5.3 9df86cb05b feat(fonts): 新增六款自托管字族(体积闸门内)(5i T9)
用户拍板「字族扩展都补」。六款按三类各两款落地,全部单页增量 ≤150KB:
小薇(文学·宋体气质)、新晰黑(文学·现代黑)、得意黑(标题)、庆科黄油(标题·圆润)、
马善政毛笔与柳建毛草(书法),许可证均 OFL。

关键取舍:@fontsource 官方中文切片粒度太粗(每片约 33 字、单片 40KB+),一句话命中
7 片即 308–442KB/页,四款全部超闸门;即便用 subset-fonts.py 按站内用字重裁仍剩
205–346KB(笔画类字族单片就 50KB)。故改为从 google/fonts 取原始 TTF,按**字频序
细切**(约 19 字/片、单片 2–12KB),每族 99–100 片、族总量 0.5–1.2MB,浏览器只拉
命中的片。四个 @fontsource 依赖随之移除,package.json 净零变更。

实测单页增量(一句话场景):xiaowei 44.9KB / huangyou 27.1KB / mashanzheng 45.7KB /
liujianmaocao 30.0KB / smiley 27.1KB / neoxihei 30.0KB——全部 PASS;
六族同页共 105KB。

接线(一处漏就静默失效,踩过两次):
- rehype-class-whitelist 放行新类(不放行则 span 的 class 被整条剥掉,页面有字没脸)
- site-content 的 ALLOWED_PAGE_FONTS 与后台排版下拉同源补齐(三处下拉 + hint)
- font-needs 扩六个 key,保证「内容用到才加载」的按需契约继续成立
- global.css 六组规则含 .mom-text 作用域(5h 补的瞬间作用域不能漏)
- typography-classes.js(后台可视化面板单一事实源)35→42 项
- subset-fonts.py 新增 --split-ttf / --slices / --order-from(hwmc 的 --css 用法未动)

覆盖边界(已在 docs 与后台 hint 写明):切片只含站内用字(约 1900 字),
新写生僻字会回落系统字族,需按 docs 命令重切。

验证: 构建 50 页 exit 0;content-lint 51 页 0/0/0;config-check 211 字段/66 hint;
      六类逐项 getComputedStyle 首项 = ZCOOL XiaoWei / ZCOOL QingKe HuangYou /
      Ma Shan Zheng / Liu Jian Mao Cao / Smiley Sans / LXGW Neo XiHei(6/6);
      六族全部出现在 document.fonts;首页字体请求数保持 17(零回归)

Spec: 2026-11-02_blog-5i-fonts-polish (T9)

Co-authored-by: GLM-5.3
2026-11-03 12:35:20 +08:00
wait4xandGLM-5.3 d096d45c58 feat(books): 书架定稿方案 C——抽书阅读 + 多本置顶默认展开
用户拍板选 C,置顶标记改为向上箭头图标(不写文字),并确认支持多本置顶。

- 抽书列表:情绪色脊 + 定宽标题列 + 展开 chevron,details 原生展开看封面与简介
- 置顶:所有 pinned 书排最前且默认展开,标记换成 ↑+横线 图标
- 删除一次性预览路由 /books-preview/ 与其 lint 白名单

验证: 构建 exit 0;content-lint 53 页 0/0/0;Playwright 实测展开/箭头旋转/
      作者列对齐/390 无溢出;两本临置顶实测均排前且同时展开

Spec: none (先行上线,完整 5i 包仍在 spec/2026-11-02_blog-5i-fonts-polish)

Co-authored-by: GLM-5.3
2026-11-03 12:28:30 +08:00
wait4xandGLM-5.3 073e124344 feat(books): 书架定稿方案 C——抽书阅读 + 多本置顶默认展开
用户 2026-11-03 拍板:选方案 C,「指定换成一个向上的箭头即可,不用表示为文字」,
并问是否支持多个置顶(答:支持,已实现并实测)。

- BooksPage 重写为抽书列表:情绪色脊 + 定宽标题列 + 作者 + 展开 chevron,
  details 原生展开显示封面与简介,零 JS
- 置顶标记改为图标:↑ + 横线的「回到顶部」字形,不写「置顶」二字,
  与右侧 chevron 形状区分;title/aria 仍带「置顶」以便读屏
- 多本置顶:所有 pinned: true 的书排在最前(沿用 sortPosts 契约)且全部
  默认展开;实测两本同时置顶 → 序列前两位、两条都 open、两个图标
- 展开/收起用 chevron(收起朝下、展开旋转 180°),reduced-motion 下不旋转
- 移动端:标题列自适应、作者隐藏、简介与封面改纵向堆叠
- 删除一次性预览路由 /books-preview/ 与 content-lint 的临时白名单;
  文档补「书架页置顶图标语义 + 多本置顶」

验证: 构建 exit 0;content-lint 53 页 0/0/0;Playwright 实测 5 行/作者列 x 唯一/
      标题零截断/点击展开且 chevron 旋转 179°/390 无溢出;
      临时两本置顶实测:顺序前两位、两条 open、图标 2 个(数据已还原)

Spec: 2026-11-02_blog-5i-fonts-polish (T7 定稿)

Co-authored-by: GLM-5.3
2026-11-03 12:27:00 +08:00
wait4xandGLM-5.3 d6e08a29f9 feat(books): 预览页新增 D 方案(封面网格 × 抽书),C 改置顶默认展开
验证: 构建 53 页 exit 0;Playwright 实测 C/D 各自唯一展开项 = 置顶书,
      D 5 格 / 3 列 / 置顶角标 1、1280 与 390 零横向溢出

Spec: none (预览页随选型迭代,正式改动在 spec/2026-11-02_blog-5i-fonts-polish)

Co-authored-by: GLM-5.3
2026-11-03 12:15:50 +08:00
wait4xandGLM-5.3 18ea5b17b8 feat(books): 新增 D 方案(封面网格 × 抽书),C 改置顶默认展开
用户反馈「B 和 C 都不错,C 方案可以默认展开置顶的书目」——故:
- 新增方案 D:格子仍是 B 的封面网格,但每格用原生 details 抽开成详情卡,
  置顶书默认展开、封面左上角带置顶角标;零 JS
- C 的默认展开从「第一行」改为「置顶书目」(frontmatter pinned: true,与文章
  置顶同一字段同一排序契约),行内显示置顶徽标
- 当前没有书被置顶,预览页用最新一本演示并明示(真实使用由后台开关决定)

验证: 构建 53 页 exit 0;Playwright 实测 C/D 各自唯一展开项 = 置顶书(等待戈多)、
      D 5 格 / 1 置顶 / 3 列 / 角标 1、1280 与 390 零横向溢出

Spec: 2026-11-02_blog-5i-fonts-polish (T7 预览迭代)

Co-authored-by: GLM-5.3
2026-11-03 12:15:34 +08:00
wait4xandGLM-5.3 d119d6b555 feat(books): 书架三案按视觉复核修订后更新预览页
长标题/作者裁切、卡片参差、4+1 孤儿位、抽书页无展开暗示等缺陷已修,
用户可直接在 /books-preview/ 看修订版。

Spec: none (预览页随选型迭代,正式改动在 spec/2026-11-02_blog-5i-fonts-polish)

Co-authored-by: GLM-5.3
2026-11-03 11:55:33 +08:00
wait4xandGLM-5.3 31fb52cb06 feat(books): 书架三案按视觉复核修完(长标题裁切/参差/孤儿位/无暗示)
视觉复核逐案挑出的缺陷全修:
A 竖排色脊:书名/作者按脊内竖向容量确定性截断(此前 10 字书名与 9 字作者挤成
  第二窄列被 40px 脊宽裁掉);脊顶上留 10px hover 余量 + 底部基线;情绪色混色
  从 16% 提到 26% 并加内描边(淡紫脊原本在白底上几乎消隐,墙被读断);短标题脊
  补竖排年份填白;移动端脊高 120→172 保持 1:4 书脊比例,不再退化成矮胖色块
B 封面网格:改 3 列(5 本排 3+2,消掉 4+1 孤儿位);标题定两行高、卡片等高
  (实测同行 394px 一致),同排基线不再锯齿;占位卡从单字换成竖排书名,视觉
  重量向真封面靠拢
C 抽书阅读:首行默认展开 + 每行加「封面与简介 ▾」暗示(此前收起态没有任何可点
  信号,看着像静态列表);去掉恒为同值的年份列;标题改定宽列 + 省略号(实测
  作者列 x 全部 518,消除锯齿);作者前加「·」并与标题拉开色阶;色条加宽成贯通行
  高的窄脊

验证: 构建 exit 0;content-lint 54 页 0/0/0;Playwright 实测 A 标题/作者零裁切、
      B 三列且同行等高 394、C 作者列全对齐 518 且首行展开、390 下脊高 172 不溢出

Spec: 2026-11-02_blog-5i-fonts-polish (T7 预览修订)

Co-authored-by: GLM-5.3
2026-11-03 11:55:32 +08:00
wait4xandGLM-5.3 6061c16396 test(e2e): 覆盖 5i 的字体加载/评论不误报/灯箱关闭/开源页动效(46→55 项)
新增九条断言,把本包修的四类回归固化成门禁:
- 字体真加载:技术轨文章的 document.fonts 必须含 LXGW WenKai Screen 与 WF-Hwmc
  (此前「class 在、计算值对、字体集里没有」是静默失效,只有这条能抓到)
- 评论不误报:进视口挂载后 #artalk-retry 必须保持 hidden,且容器打上 mounted 标记
- 灯箱关闭:点 #lb-close 后 dialog 必须变 closed(此前被右侧全高侧栏吃掉点击,
  表现是点了没反应,只能靠这条兜住)
- 开源页:零第三方外链图 + 打字机存在 + 数据板有自托管资产

验证: E2E_BASE=http://127.0.0.1:18934 node tests/e2e.mjs → 55/55 PASS

Spec: 2026-11-02_blog-5i-fonts-polish

Co-authored-by: GLM-5.3
2026-11-03 11:50:10 +08:00
wait4xandGLM-5.3 cfe508ff14 fix(opensource): 修视觉终审的四个 P1 与灰阶/对齐细节
视觉终审对开源页改版判「需修」,逐条修完并量化复验。

P1-1 数据板空档与三段右缘不齐:streak 只占半栏、snake 通栏 → 右侧空 306px。
- 网格改 `repeat(auto-fit, minmax(320px, 1fr))`,仅一张非通栏卡时加 `.pf-card-solo`
  (`grid-column: 1/-1`),语言条去掉 max-width
- 实测:统计行/语言条/数据板右缘统一 959px,streak 卡宽 298→638

P1-2 卡内 SVG 文字不可读:streak 固有 495px 被压到 298px(0.6×),14px 标签变 8.4px。
- 卡改全宽后 1:1 渲染(实测 rendered=495=natural);抓取参数把次级灰换成站点 ink:
  亮色 #3f424a(10.05:1)、暗色 #b9bec8(9.07:1),旧值 #9aa0aa 仅 2.63:1

P1-3 移动端打字机折行推挤:390 下第 4 句 260px > 可用 230px 必折两行,
min-height:1.6em 挡不住 → 每轮下推 21px 再弹回。
- `@media(max-width:560px){.typing{min-height:calc(1.6em*2)}}`(实测 42px 恒定零推挤)
- 光标收成 2px 竖条 + baseline 对齐(原来是 0.5em 实心块,两行时被拉到行间)
- typing 第 4 句裁短(中文 ≤16 字、英文 ≤36 字符)

P1-4 streak 卡入场动画空壳帧:SVG 内 fadein 序列(环 0.4s/数字 0.6s/…/1.2s)+ lazy
加载 → 用户先看到 0.5–1.5s 半成品,截图也会随机拍到空卡。
- 抓取脚本新增 freezeAnimations():剥掉全部 animation 声明、opacity:0 改 1、
  删除 @keyframes;重抓后 grep animation:/opacity: 0 均为 0(streak 各 6KB、
  snake 35KB→20KB)

P2 一并:
- streak 亮色容器 grayscale(1) 回到灰阶基调;暗色蛇卡改 grayscale+invert+contrast+
  brightness 组合,空网格不再与底色同色
- 项目墙描述 text-wrap: pretty(长描述末行孤词)
- 统计口径注释改为「三格」,不硬塞第 4 格(贡献数已由热力图承载)
- 社交图标行负 margin 做光学对齐

附:content-lint 增加 SKIP_ALL 白名单(一次性预览路由 /books-preview/ 无 en 对应页,
语言切换链接会误报断链+配对缺)

验证: 构建 51 页 exit 0;content-lint 54 页 0/0/0;E2E 46/46;
      Playwright 实测三段右缘 959 一致、streak 1:1、1280/390 零横向溢出、
      移动打字机高度 42px 恒定、光标 2px、项目墙 text-wrap=pretty

Spec: 2026-11-02_blog-5i-fonts-polish (T8 修复)

Co-authored-by: GLM-5.3
2026-11-03 11:48:25 +08:00
wait4xandGLM-5.3 b8c5e1c86e feat(books): 书架三方案预览页先行上线(临时路由)
用户要「直接查看网页」来选方案,故本页单独先部署;其余改版(字体按需加载、
灯箱关闭与 caption 排版、评论误报、开源页动效)仍在验证分支,选定方案后一并
合并并删掉本临时路由。

Spec: none (预览页需先上线供用户选择,其余改动仍在 spec/2026-11-02_blog-5i-fonts-polish 验证)

Co-authored-by: GLM-5.3
2026-11-03 11:42:25 +08:00
wait4xandGLM-5.3 907f64472a feat(books): 书架改版三方案预览页(临时路由)
用户要求「书架页多做出几个方案让我直接查看网页供我选择」,故先并陈三案:
A 竖排色脊墙(书脊 writing-mode 竖排、情绪色即是视觉线索)
B 封面网格(封面为主角,无封面用色脊占位 + 首字,悬停浮起并显出简介)
C 抽书阅读(一排书脊,点开原生 details 抽书,显示封面/简介/入口)

同一份数据渲染三案(posts category=reading),页顶 sticky 锚点跳转。
用户选定后:把选中方案落进 BooksPage.astro 并删除本临时路由。

验证: 构建 exit 0;dist/books-preview 27KB;Playwright 实测三案均渲染
      (脊 5 / 卡 5 / 抽书行 5)、1280 与 390 均零横向溢出、破图 0

Spec: 2026-11-02_blog-5i-fonts-polish (T7 预览)

Co-authored-by: GLM-5.3
2026-11-03 11:41:10 +08:00
wait4xandGLM-5.3 6293fe25d5 chore(spec): 5i 勾选 T1
Spec: 2026-11-02_blog-5i-fonts-polish

Co-authored-by: GLM-5.3
2026-11-03 11:28:51 +08:00
wait4xandGLM-5.3 8d82aab213 fix(fonts): 字体切换真加载——按内容诉求注入,含瞬间排版类作用域缺口(5i T1)
用户实测「字族渲染没有成功,尤其是字体的切换」。根因不是字体没做,是加载门禁错:
BaseLayout 把 serif/kai/kuaile/hwmc 四份字体 CSS 全关在 `track === 'reading'` 分支里,
技术轨文章与全站页面(默认 track='tech')从不加载它们——于是 `.font-kai` 计算值虽是
`"LXGW WenKai Screen"`,但该 @font-face 根本不在文档字体集里,浏览器静默回退系统宋体。

- 新增 `src/lib/font-needs.ts`:`fontsFromText()` 纯函数 + `fontsForPage(pathname, track)`
  按路径取该页内容(文章正文 / home.yaml / about.yaml / opensource.yaml / 全部瞬间文本)
  与页面静态诉求(reading→serif;书架标题写死楷体→kai 等)算出所需字族集合
- BaseLayout 改为逐项按需注入(非阻塞 preload+onload 写法保留),noscript 同步;
  汇文明朝仍走 public 手工切片路径,network/font-display 未动
- **顺手补掉 5h 遗留的用户可见缺口**:瞬间富文本容器 class 是 `mom-text`,而全局排版类
  规则只覆盖 `.prose-*/.rich/.prose-body` → 瞬间里写 `font-kai`/`c-teal` 一直只有 class
  没有样式(新瞬间正是这么写的,用户看到的就是"没变色没换字")。global.css 给 38 处
  选择器组补 `.mom-text`
- 另修自写 bug:en 路由切分 `slice(3)` 残留前导 `/` 导致 en 页按需字体全部落空,改 `slice(4)`

验证: 构建 exit 0(content-lint 51 页 0/0/0);E2E 46/46;Playwright 实测
      技术轨文章 document.fonts 命中 LXGW WenKai Screen / WF-Hwmc / ZCOOL KuaiLe /
      Noto Serif SC,`.font-kai` 计算族 = LXGW 且 check=true;
      首页仍为基线(零增量);瞬间页因有 font-kai 内容而命中 LXGW;
      书评页只拉 serif(41 片,0 kai 0 汇文)

Spec: 2026-11-02_blog-5i-fonts-polish (T1)

Co-authored-by: GLM-5.3
2026-11-03 11:28:51 +08:00
wait4xandGLM-5.3 029633f00f chore(spec): 5i 勾选 T6/T8
Spec: 2026-11-02_blog-5i-fonts-polish

Co-authored-by: GLM-5.3
2026-11-03 11:24:29 +08:00
wait4xandGLM-5.3 0ff4ede91f feat(opensource): 项目墙恢复居中,README 动效以自托管方式回归(5i T6/T8)
用户两项要求:开源页 Projects「依旧采取居中即可,不用左对齐」;README 迁移后
「很多图片元素还有一些 svg 基本上都摒弃了,可以在 readme 的基础上进行再创新,
保留那些动效的图,以及创新更多一些显示方式」。

T6 项目墙居中:`.repo` 从 align-items:stretch/text-align:left 改回 center/center,
`.repo-desc` 加 max-width:34em 防居中后长描述散架;语言点保持灰阶(与对齐无关)。

T8 动效回归(零运行时外链):
- 新增 geek/TypingLines.astro:自绘打字机(SSR 先出首行防闪白,JS 逐字打出/删除/
  轮播,光标 CSS 闪烁,prefers-reduced-motion 与单条文案时保持静态,min-height
  防抖,astro:page-load 重挂兼容软导航)——替代 readme-typing-svg 外链,且跟随主题
- 新增 scripts/build-perf/fetch-readme-assets.mjs:抓 stats/top-langs/streak/snake
  到 public/github-assets/(10s 超时 + UA、透明底、亮暗两套站点色),产物入仓保证
  离线可构建;单资产失败保留旧文件、无旧文件则该卡不渲染
- GithubProfile 新增「数据板」:灰阶 hairline 卡 + mono 小标题 + 2 列网格(移动单列),
  与原生统计四格、语言占比、热力图组合成展示区;贡献蛇在暗色加 grayscale+invert
  滤镜(SVG 内部是自带绿阶,直接贴暗底刺眼)
- build.sh 只做存在性守卫 + 缺资产 WARN,**不挂抓取**(发布链不被外网抖动拖累)
- 后台开源页新增 typing 字段(list,zh/en 双语,≤6 条,summary 取 zh)+ schema 声明

验证: 构建 51 页 exit 0;config-check 211 字段/66 hint 全过;E2E 46/46;
      产物断言第三方外链零命中、本地引用 streak-light/streak-dark/snake;
      Playwright 实测打字机文本 900ms 内变化(动效在跑)、streak naturalWidth=495、
      snake=880、暗色自动切 streak-dark、项目墙 center/center、1280 与 390 均零横向溢出

备注: github-readme-stats 今日全局 503,stats/top-langs 两卡按契约跳过(原生四格与
      语言条已覆盖同一信息);服务恢复后跑一次 fetch-readme-assets.mjs 即自动补上。

Spec: 2026-11-02_blog-5i-fonts-polish (T6/T8)

Co-authored-by: GLM-5.3
2026-11-03 11:24:28 +08:00
wait4xandGLM-5.3 23303bd39a chore(spec): 5i 勾选 T2-T5
Spec: 2026-11-02_blog-5i-fonts-polish

Co-authored-by: GLM-5.3
2026-11-03 11:18:36 +08:00
wait4xandGLM-5.3 31eb44caee fix(comments): 评论失败提示误报与瞬间评论区配色脱同步(5i T2/T3)
用户实测:「评论区总是显示加载失败,但是实际上是正常显示的,另外瞬间的评论区
的颜色和主题色不同步」。

T2 误报根因:软导航/二次挂载时 `#artalk-lib` 脚本标签已在 DOM,但 `window.Artalk`
尚未就绪,旧代码直接 `initArtalk()` → `!lib` 分支 `showRetry(true)` → 失败提示
永久显示;随后库加载完成、评论正常渲染,提示却留在页面上。
- 脚本标签在但库未就绪 → 等它的 load 事件再 init,不再立即判失败
- 失败提示只留真失败:script.onerror,或超时且确实未挂载
- `mounting` 在所有成功/失败路径复位(旧代码失败后永真,把 IO/锚点/滚底/重试全堵死)
- 成功路径清提示;三条触发路径与重试钮、astro:page-load 复核全部保留

T3 配色脱同步:18 个 `--at-color-*` 映射原先只写在 `#artalk-container` 作用域,
瞬间容器是 `mom-atk-<slug>` 不在其内 → 回落到 Artalk 默认白底。
- 抽成共享片段 geek/ArtalkThemeStyles.astro(两处引用,杜绝双份漂移)
- **额外挖出的第 4 个坑**:Artalk init 成功会给容器加 `.artalk` 类,其 CDN 样式表
  里 `.artalk{--at-color-bg:#fff}` 与裸属性选择器 `[data-artalk-panel]` 同特异性
  且后加载 → 平级后来居上。瞬间容器选择器加 `body` 前缀 (0,1,1) 压住;
  文章页是 `#artalk-container`(1,0,0) 本来就压得住,所以只有瞬间翻白

验证: 构建 exit 0;E2E 46/46;我独立复测——
      ① 文章页挂载成功且 #artalk-retry 保持 hidden
      ② 连发 3 次 astro:page-load 仍不误报、仍挂载
      ③ 瞬间评论区明暗两态 --at-color-bg 实测 rgb(255,255,255)/rgb(25,29,35)
         分别等于 --bg,容器带 .artalk 类(说明是 init 后真态)

Spec: 2026-11-02_blog-5i-fonts-polish (T2/T3)

Co-authored-by: GLM-5.3
2026-11-03 11:18:35 +08:00
wait4xandGLM-5.3 5c015c6577 docs(writing): 取消彩色强调次数限制,补灯箱 caption 排版说明
用户原话:「色彩强调不要设置限制,取消一页不超过三处的限制」。原「一页 ≤3 处」
是文档纪律(代码层从未硬性拦截),故改动集中在文档、模板文章与后台提示:

- 写作指南 / 管理台:改为「彩色不限次数」,同时明确仍守的另一条红线是
  **外壳灰阶**(导航/页脚/按钮/背景不染色)——外壳是气质,正文是表达
- 写作模板文章中英双语同步:章节改名「想用多少用多少」,删掉"三处之后不再上色"
  的示范约束,表格里「一页 ≤3 处」改为「不限次数(外壳仍灰阶)」
- 顺带补灯箱 caption 文档:照片说明支持 markdown + 排版类,深底自动取暗档;
  以及灯箱四种关闭路径(右上 ×、Esc、点背景、1× 时点图)

验证: 构建 exit 0;全仓 grep 「≤3 处/页」只剩「已取消」的历史说明

Spec: 2026-11-02_blog-5i-fonts-polish (T5)

Co-authored-by: GLM-5.3
2026-11-03 11:18:14 +08:00
wait4xandGLM-5.3 138d3d976b fix(gallery): 修灯箱右上关闭点不动,caption 支持字体与颜色
用户实测:「图库中的图片点击查看之后,点击右上角的关闭无法关闭,其次就是右侧
的文字是否支持字体的更换和颜色的更换」。

关闭失灵的根因(生产实测取证):桌面端信息栏 `.lb-l2` 是 top:0→bottom:0 的
右侧全高侧栏(宽 200px),且 DOM 顺序在关闭钮之后;`document.elementFromPoint`
在按钮中心命中 `.lb-meta.lb-l2` —— 点击被侧栏吃掉,`lb.close()` 永不触发
(实测 Playwright 点击 #lb-close 30s 超时、dialog 保持 open)。给 `#lb-close`
提层级 z-index:12 后「点击 → open=false」通过。

caption 富文本:
- 照片 caption 走全站同一条管线(markdown → 内容净化三层白名单 → class 白名单),
  构建期预渲染成 HTML 挂在卡片的 data-caption-html 上,换图时写 innerHTML
- 灯箱容器加 `.rich`,global.css 的 .rich .c-blue / .rich .font-kai 才生效(此前
  只有触发类名的选择器、容器无 .rich 前缀,故颜色字体都不生效)
- 深底适配:灯箱内的彩色取柔和对暗底的档位;段间距收紧、首尾不撑高
- 容器由 <p> 改 <div>:caption 可含多段落,<p> 套 <p> 会被浏览器拆散

验证: 构建 exit 0;生产同款探针实测关闭生效(此前超时);
      注入测试 caption 后产物含 class="c-blue"(保留)、font-kai(保留)、
      **加粗**→<strong>、<script> 被净化丢弃(验收数据已还原)

Spec: 2026-11-02_blog-5i-fonts-polish (T4)

Co-authored-by: GLM-5.3
2026-11-03 11:18:13 +08:00
wait4xandGLM-5.3 d30b5decbb docs(spec): Phase 5i 立项——字体加载根治与交互缺陷修复
七项用户实测问题的取证结论写入 spec:字体切换失效根因是 BaseLayout 把四份
字体 CSS 关在 track==='reading' 里(技术轨与全站页面从不加载);Artalk 失败提示
误报源于软导航后脚本标签存在但库未就绪即判失败;灯箱右上关闭被 .lb-meta 命中
遮挡(elementFromPoint 实测)。任务拆 9 项,字族款式与书架方向待用户拍板。

Spec: 2026-11-02_blog-5i-fonts-polish

Co-authored-by: GLM-5.3
2026-11-03 11:03:32 +08:00
wait4x 2526b98323 Update 页面文案 “home” 2026-11-03 02:41:48 +00:00
wait4xandGLM-5.3 36643f5cf0 chore(spec): 合入 CMS 侧内容编辑并解决 opensource.yaml 冲突
合远端 main(后台 CMS 期间产出 3 个「Update 页面文案」提交)时的两处冲突:

- blurb_after:取 CMS 版本(用户的编辑为准);折号间隙已由模板
  `{a}{' '}{blurbAfter.trim()}` 保证,不再依赖数据前导空格
- opensource.yaml 尾部:CMS 侧把 perSource 改成 github、intro_from 切到
  github-readme。**perSource 尊重用户选择**;intro_from 改回 "off" 并补注释——
  本阶段的目的正是用原生档案卡替换 README 原样渲染(README 带 7 类第三方外链图,
  且 `<div align="center">` 在本站失效,正是用户报告的「没居中、图片不显示」根因)。
  想回退在后台把该字段切回 github-readme 即可。

顺带对齐:关于页「找到我」的邮箱原来是 admin@wait4x.online(我拟的),
改为与站点 socials 一致的 X@alumni.nudt.edu.cn,避免出现一个不存在的地址。

验证: 构建 51 页 exit 0;content-lint 51 页 0/0/0;config-check 全过;
      E2E 46/46 PASS

Spec: 2026-11-02_blog-5h-content-social

Co-authored-by: GLM-5.3
2026-11-02 21:21:41 +08:00
wait4x 52188d5b12 Merge branch 'spec/2026-11-02_blog-5h-content-social'
Phase 5h 交付:文章置顶(sortPosts 统一口径 + 灰阶 pin 徽标 + 后台开关)、瞬间富文本(复用净化管线,字体与颜色可换)、瞬间逐条评论(展开才挂 Artalk,评论键用虚拟路径避开 # 截断)、浏览/评论计数徽标(眼睛+气泡本地 SVG,修同载双计与同页数字不一致)、开源页去 README 外链依赖(原生 GitHub 档案卡 + 语言占比灰阶 + 修 GitHub API 无 token 被打满)、关于页承接个人叙述、写作模板示例文章(中英双语,本身即置顶活样本),以及视觉终审两轮修复(日期折行、emoji 豆腐块、移动导航折行、灰阶纪律清剿)。

Spec: 2026-11-02_blog-5h-content-social
2026-11-02 21:16:43 +08:00
wait4xandGLM-5.3 fef2de1d37 chore(spec): 归档 blog-5h——七任务收敛
Spec: 2026-11-02_blog-5h-content-social

Co-authored-by: GLM-5.3
2026-11-02 21:16:42 +08:00
wait4xandGLM-5.3 bdaf02f9ca fix(opensource): 项目墙卡片左对齐与 bio 间隙兜底(视觉终审二轮)
Finding 1(P1)项目墙卡片内容被居中:
- 根因是全局 `.tap { display:inline-flex; align-items:center; justify-content:center }`
  沿列向 flex 的交叉轴生效,`.repo`(scoped)只赢下 display 没接管 align-items,
  于是标题与「● 语言 ★ n」行落在卡中部、卡片间左缘参差
- `.repo` 显式 align-items: stretch + text-align: left;实测四张卡的
  .repo-name 与 .repo-meta 左缘全部 313px(= 卡左 296 + 内距 16 + 边框 1)

Finding 2(P2)bio 破折号间隙修在数据不在模板:
- 原靠 opensource.yaml 串前导空格产生间隙,后台清空走兜底串时缺陷回归
- 模板改为 `{a}{' '}{blurbAfter.trim()}`:间隙由模板保证,数据怎么改都不会贴链接

验证: 构建 51 页 exit 0;content-lint 0/0/0;spec_verify 七项 exit=0(新指纹
      62086ce4cccd);check 收敛;DOM 实测对齐与间隙均符合预期

Spec: 2026-11-02_blog-5h-content-social (视觉终审二轮)

Co-authored-by: GLM-5.3
2026-11-02 21:14:37 +08:00
wait4xandGLM-5.3 c7d1e7b975 fix(moments): 评论流键从 #<slug> 改为虚拟路径,避免键被截断
实测发现 `#` 在查询串里是 fragment 分隔符:裸传 `/moments/#2026-11-02` 会被服务端
截断成页面级键 `/moments/`(生产 Artalk 实测,响应里 key 变成 "/moments/"),
后果是所有瞬间共用同一个评论流、pv 也串在一起。

- page_key 改为虚拟路径 `/moments/<slug>/`(生产实测键原样保留,零转义风险;
  将来真做单条瞬间页时键天然对齐)
- spec 与 runs.jsonl 口径同步(七项 verify 在新指纹下重跑全绿)

验证: 构建 51 页 exit 0;产物含 /moments/2026-11-02/ 键;content-lint 0/0/0;
      spec_verify 七项 exit=0;check 收敛

Spec: 2026-11-02_blog-5h-content-social (T3 修正)

Co-authored-by: GLM-5.3
2026-11-02 21:05:51 +08:00
wait4xandGLM-5.3 550f6d15b4 chore(spec): 5h 任务全勾与运行证据入库
- tasks.md 七任务全勾;spec.md 功能框与成功标准全勾
- §5.1 全项目检查补升级条件(触及 BaseLayout/全局样式或 Artalk 接管层才升级)
- runs.jsonl 记录七项 verify 全部 exit=0(含本包新增的 E2E 46 项口径)
- content/.link-cache.json:链接卡片缓存随新文章构建刷新(入仓保证构建确定性)

Spec: 2026-11-02_blog-5h-content-social

Co-authored-by: GLM-5.3
2026-11-02 21:01:47 +08:00
wait4xandGLM-5.3 d4fe273812 fix(visual): 修视觉终审的折行/豆腐块与灰阶纪律违规
视觉终审(glm-5.3-flash:max)判 BLOCK,逐条修完并复测。

P1 阻断:
- 瞬间页日期折成两行(`2026-` / `04-29`):日期列复用 var(--space-5)=40px,
  而 10 字符 mono 日期需 ≈75px 且缺 nowrap。抽 --tl-rail(5.4rem) 统一驱动
  轨道宽度与两处偏移,加 nowrap;实测日期单行(高度 20px)且圆点与卡片对齐

P2 交付可见破形:
- 关于页「关于我」行首 emoji 在无彩色 emoji 字体的环境渲染成黑方块:
  正文行首 emoji 全部去掉(改纯文字段落),并给 --font-serif/--font-sans
  链尾补 Apple/Segoe/Noto emoji 回退——用户内容里的 emoji(瞬间、书评)
  同样不再落豆腐块

移动端导航两字折行(开/源、文/章):
- ≤450px 的 overflow-x 内滚从未生效:flex 子项默认可压缩,中文词被压到就地
  折行。给 nav ul > li 加 flex:none + .nav-link nowrap;实测 360/390/430
  三档全部单行(Range.getClientRects 计数逐项验证)

灰阶纪律:
- 项目墙原来按 GitHub linguist 给每个仓染语言色点,开源页铺 15 个仓 = 15 种
  彩色,破「外壳灰阶、彩色只作正文强调」红线 → 统一改灰阶点,语言靠文字承载
- 语言占比并列(Python/TypeScript 各 36%)只有第一名着色,读起来像谁领先 →
  判定改为「百分比等于最大值者一并着色」

版式与文案细节:
- prose 标题到正文比 tracks 多一档(16+16=32 vs 16)→ .sec-title + .prose-body
  margin-top: 0
- 开源页 bio 破折号贴链接(`blog—— Astro`)→ 破折号前补空格
- 页脚 built from<hash> 缺空格(Astro 吃掉换行处空白)→ 显式 {' '}
- 新增一条真实瞬间(含青色楷体富文本)替代只有占位串的样本,让评论区与标签行
  的间距可在真实数据下验收

验证: 构建 51 页 exit 0;content-lint 51 页 0/0/0;E2E **46/46 PASS**;
      390px 四页 scrollWidth == innerWidth(无横向滚动);
      导航 360/390/430 全部单行;日期单行;config-check 全过

Spec: 2026-11-02_blog-5h-content-social (视觉修复)

Co-authored-by: GLM-5.3
2026-11-02 20:56:00 +08:00
wait4xandGLM-5.3 0b0307b12f docs(admin): 补档案卡与计数徽标说明(5h T7 文档收尾)
- 管理台「开源页」节:说明开篇已改原生 GitHub 档案卡(零第三方图片、
  语言占比灰阶、取数失败只隐藏该项),并把 intro_from: github-readme
  标为「仍可用但不推荐」——点名 7 类外链图与 align 居中失效正是用户
  报告的"没居中/图片不显示"根因
- 新增「浏览与评论计数」节:眼睛/气泡含义、一页只计一次(其余为只读镜像,
  不会出现两个不同数字)、开评论不算浏览、接口挂掉显示「–」不给假零、
  换页重计属定义而非双计

验证: pnpm astro build exit 0

Spec: 2026-11-02_blog-5h-content-social (T7 收尾)

Co-authored-by: GLM-5.3
2026-11-02 20:37:01 +08:00
wait4xandGLM-5.3 3e377adc7f feat(opensource): 开源页改原生档案卡,个人叙述迁往关于页(5h T5/T6)
用户原话:开源页开篇是从 GitHub README 直接移植的,「部分内容没有居中,有些图片
也没有正常显示,可以自行进行美化,无需完全一致……有些可以直接移植到关于页面」。

病根:README 是 GitHub 专用 markdown——`<div align="center">` 靠 HTML 属性居中
(本站按普通块渲染,故"没居中"),且依赖 7 类外链图(readme-typing-svg /
shields.io / github-readme-stats / streak-stats / activity-graph / snake /
komarev),慢网或被墙时全空白(故"图片不显示")。

T5 开源页重构:
- 新增 geek/GithubProfile.astro:头像 / 名字@handle / 定位叙述 / 社交图标 /
  统计四格(followers、公开仓、总星标)/ 语言占比灰阶条(仅第一名 accent,
  其余 tokens 灰阶)——**零外链图、零 iframe、零第三方徽章**
- 语言聚合复用 loadProjects 的结果;followers 与公开仓数走 GitHub REST
- 任一取数失败只隐藏该项(catch + warn),绝不炸构建
- opensource.astro / en:档案卡承接原 README 位置 → 热力图 → 项目墙;
  README 渲染降级为兼容通道(intro_from=off 时不执行),yaml 注释写明
  7 类外链图与 align 失效原因并标「不推荐」
- **修既有隐患**:GitHub API 请求原先不带 token,匿名 60 次/小时在密集构建下
  被打满 → 项目墙退化成兜底 JSON(今天一度只剩 1 条)。现对 api.github.com
  自动附 Bearer(token 读宿主 0600 文件,不入仓),墙恢复 16 条、总星标 0→66、
  语言占比变真值

T6 关于页承接:
- about.yaml blocks 扩为 prose / tracks(兴趣版图)/ prose(关于我)/
  linklist(找到我:本站·邮件·GitHub·自建 gitea·RSS)/ prose(本站如何构建),
  双语齐备;AboutPage 零改动(blocks 早已驱动)
- 原则落地:关于页讲人,开源页讲开源,两边不再重复

验证: 构建 51 页 exit 0;content-lint 51 页 0/0/0;config-check 全过;
      E2E **46/46 PASS**(37 原有 + 本包新增 9 项:置顶顺序与徽标、文章页计数、
      模板彩色与折叠、瞬间评论区与只读计数、关于页块驱动);
      dist/opensource 七类第三方外链零命中、第三方 img 数 0;数据实值:
      语言占比 Python/TypeScript 各 36%、Shell/Rust/JS 各 9%

Spec: 2026-11-02_blog-5h-content-social (T5/T6)

Co-authored-by: GLM-5.3
2026-11-02 20:35:56 +08:00
wait4xandGLM-5.3 63c9c8ad4a feat(counts): 浏览/评论徽标接入文章与瞬间页,修同载双计(5h T4)
用户要「加一个访客数量、评论数量,采用类似于开源页面从 GitHub 移植过来的那个
眼睛图标」——落地为 ViewMeta 本地 SVG 徽标,无第三方图片依赖。

接入(op-artalk 车道):
- 文章详情页 reading/tech 两分支各挂一枚 mode="page"(两分支互斥,同页仅一处计数)
- ArtalkComments 顶部旧的纯文本 pv 行与自成一套的 bumpPv 脚本整段删除,改为
  readonly chip:评论区只读展示,不再各计一次
- 瞬间页顶部整页 pv 行同样改 chip;每条瞬间卡片底部为 readonly 徽标

组件修复(本人,op-artalk 实测揪出):
- **同载双计**:脚本既注册 astro:page-load 又立即调用 run(),首屏两者都会触发
  → 每元素 POST 两次、浏览量翻倍(实测 15→17→19 每载 +2)。加每元素一次性
  守卫后 1/2/3 每载 +1;软导航新元素仍照常计数
- **同页两枚数字差 1**:readonly 实例独立 GET 到的 pv 可能比计过数的少 1;
  改为同键存在 page 实例时 readonly 让位,由 page 实例回填同一份结果
- page 实例请求失败时释放兄弟自读,避免评论区永远停在「–」

验证: 构建 exit 0;E2E 37/37;Playwright 拦截对账——
      每载 POST=1、同页两枚徽标文本一致(101/101)、刷新后累计 2、
      接口全挂时两枚均显示占位「–」不报错

Spec: 2026-11-02_blog-5h-content-social (T4)

Co-authored-by: GLM-5.3
2026-11-02 20:29:44 +08:00
wait4xandGLM-5.3 8e74bead62 feat(moments): 瞬间富文本与逐条评论(5h T2/T3)
瞬间文字从纯文本直出升级到与文章同级表达力,每条动态获得独立评论流。

T2 富文本:
- text 在 frontmatter 预渲染成 momentId → html 的 Map(renderRich 是 async,
  模板内不能 await),模板只此一处 set:html 且喂的是净化产物
- 走与全站文案同一条管线:markdown → 内容净化(标签/属性/协议三层白名单)
  → class 白名单 35 项,故 <span class="c-blue font-kai"> 生效、
  <script> 整棵子树被丢弃(实测)
- 纯文本老数据零回归:无 markdown 的 text 得到单个 <p>,与旧直出视觉等价
- 卡片内富文本首尾 margin 归零、段间半档、列表缩进 1.35em,15px/ink-2 基调不变

T3 逐条评论:
- 新增 MomentComments.astro:<details> 折叠,**展开才拉 Artalk 并 init**
  (未展开连 Artalk.js 都不下载,时间线一屏十几条不白付成本)
- page_key = /moments/#<slug>,中英双语共用同一评论流(与文章页同策略)
- pvAdd: false 且复用 ViewMeta readonly 计数:读者开评论不会虚增浏览数
- 5s 兜底失败提示 + 重试;软导航回页对已展开的框补挂
- Playwright 实测同页两个容器各自 init 成功(各渲染 31 个 atk-* 节点,无异常),
  故无需降级;若日后内存吃紧可对上一实例调 destroy() 一行切换

验证: 构建 exit 0;E2E 37/37(快照口径);临时瞬间实测 markdown 粗体、
      彩色+楷体类保留、行内码、<script> 净化丢弃、details 与 /moments/# 键、
      ViewMeta 只读挂载全部命中(验收数据已还原)

Spec: 2026-11-02_blog-5h-content-social (T2/T3)

Co-authored-by: GLM-5.3
2026-11-02 20:20:31 +08:00
wait4xandGLM-5.3 5bf0c327bf feat(content): 写作模板示例文章与文档(5h T7 主体)
用户要「在文章中加一篇关于文章书写的模板示例」,故交付一篇**可复制**的模板
文章而非干条文:正文每节都在真跑一种能力,读者看到的效果就是源码的效果。

- content/posts/writing-template.md(zh)与 posts-en 同名 en 版(locked:true
  手写直译,不交给 TMT 重译):标题层级纪律、彩色强调(一页三处限额)、
  字族(楷/汇文明朝各一处 + 六族清单)、引用/列表/代码窗条/表格/图片放大/
  链接卡片/details 折叠/分隔线
- pinned: true —— 这篇文章同时是置顶功能的活样本(归档首条实测)
- 后台 moments.text 由 text widget 改 markdown widget + 排版类提示:瞬间从此
  与文章同级表达力(字体/颜色/多段),提示里写明外壳灰阶纪律
- docs/写作指南.md 新增三节:写作模板(cp 起步 + 三条纪律)、置顶文章
  (语义边界:只改顺序不改 pubDate/URL/RSS)、瞬间怎么写(富文本 + 逐条评论
  + 只读计数 + text/image 至少一项)

验证: pnpm astro build 51 页 exit 0;content-lint 51 页 0 断链 0 缺图;
      产物断言 c-blue/font-kai/font-huiwen/details/table 全部生效;
      dist/archives 首条 = writing-template(置顶排序生效)

Spec: 2026-11-02_blog-5h-content-social (T7)

Co-authored-by: GLM-5.3
2026-11-02 20:08:21 +08:00
wait4xandGLM-5.3 6538311387 feat(content): 置顶契约与计数徽标组件(5h T1 契约部分)
置顶(T1):
- src/lib/post-order.ts 新增 sortPosts:置顶优先 → pubDate 倒序 → id 兜底,
  把首页双轨/归档/书架四处排序口径收敛到一处
- postsSchema 增 pinned;PostList 三处渲染路径加灰阶 pin 小徽标;后台置顶开关
- 语义边界:只影响列表顺序,不改 pubDate/URL/RSS 时间语义

计数徽标(T4 契约组件):
- src/components/geek/ViewMeta.astro:眼睛 = 浏览数、气泡 = 评论数,
  本地 SVG + tokens 着色,不引第三方图片
- 数据只用 Artalk 匿名端点:GET comments?limit=1 一次拿到 count 与 page.pv;
  page 模式先 POST pages/pv 计一次浏览,readonly 模式零写入
- SSR 占位「–」:无 JS/接口失败不显示 0 误导读者

验证: pnpm astro build 48 页 exit 0;config-check 208 字段/65 hint 全过;
      sortPosts 四调用点替换后构建通过

Spec: 2026-11-02_blog-5h-content-social (T1 契约)

Co-authored-by: GLM-5.3
2026-11-02 20:02:08 +08:00
wait4x cdd9c61641 Update 页面文案 “home” 2026-11-02 11:47:24 +00:00
wait4x e1fc8bd64c Update 页面文案 “opensource” 2026-11-02 11:45:26 +00:00
wait4x cc12e016ab Update 页面文案 “opensource” 2026-11-02 11:42:51 +00:00
wait4xandGLM-5.3 ed473e996a fix(admin): 主题设置集合结构修复 + 集合结构构建校验
用户实测后台报错:「主题设置 集合: The file option content/site/theme.yaml is
invalid. It must be the path to a JSON file」——根因是 site_theme 把 `file:`
直接写在集合层级,而 Decap/Sveltia 的文件集合必须是
`files: [{ name, label, file, format, fields }]` 结构;缺 files 包装时 Sveltia
按单文件变体解析并期待 JSON 路径,故报该错。

- site_theme 改为标准 files 集合(format: yaml 显式声明)
- site_pages 三单例补显式 format: yaml(扩展名推断可用但显式更稳)
- config-check.py 增加结构断言:每个集合必须 folder 或 files 二选一、files
  项必须齐 file/label/fields、file 必须指向真实文件——这类"后台整体打不开"
  的结构错从此在构建期拦截

验证: config-check 五文件全 OK(207 字段/64 hint)+ 结构断言通过;
      pnpm astro build exit 0

Spec: none (后台配置结构修复,构建校验加固)

Co-authored-by: GLM-5.3
2026-11-02 19:35:28 +08:00
wait4x c772236d1a Merge branch 'spec/2026-11-02_blog-5g-pages'
Phase 5g 交付:块式页面构建器(关于页九类区块驱动)+ 样式即数据(theme.yaml + 对比度闸门)+ 首页抽 HomePage 消双份与三字段 + 开源页收尾(README 四级回落/兜底 8 仓/可见性告警)+ 后台文档与 hint(config.yml 重复键结构缺陷修复)+ 字体子集化(汇文 -40%)+ 全站三层排版(富文本/页面级覆盖/艺术字体类)。

Spec: 2026-11-02_blog-5g-pages
2026-11-02 19:20:17 +08:00
wait4xandGLM-5.3 a89c443c25 chore(spec): 归档 blog-5g——七任务收敛
Spec: 2026-11-02_blog-5g-pages

Co-authored-by: GLM-5.3
2026-11-02 19:20:17 +08:00
wait4xandGLM-5.3 23b79a2473 chore(spec): blog-5g 全任务勾选(T4 措辞修正)
Spec: 2026-11-02_blog-5g-pages

Co-authored-by: GLM-5.3
2026-11-02 19:18:03 +08:00
wait4xandGLM-5.3 8274180f81 docs(admin): 后台文档与 hint 补齐 + 修复 config.yml 重复键结构缺陷(5g T6)
- docs/管理台.md 扩写 25→156 行(原文要点全保留,131 增 0 删):四集合
  总表、块编辑九类(字段+示例 yaml+预览限制)、页面排版覆盖(含整页换字体
  的体积成本提醒)、theme 17 旋钮分组约束、首页三字段、开源页六字段 +
  include 未命中排查 + README 回落矩阵四级(把 T5 的"相对链接不重写/
  徽章不阻断"两条限制落到用户文档)
- docs/写作指南.md:首页可配项补三行、新增「页面块」字段速查节(含 ≤24
  块/≤4000 字/未知块跳过/空则回落 intro 的边界)、字体子集化段
  (694KB→420KB −40% + subset-fonts.py 命令)、加字流程第④步挂子集化
- 后台 hint 49→64:九类块型说明、tagline/blurb、font 下拉体积提醒、scale
  取值范围
- **修 config.yml 结构缺陷(重要)**:typography 对象被插进双语字段的
  name: 与 widget: object 之间,导致重复键 widget/fields(后者覆盖前者)→
  后台 tagline/blurb 退化成字符串写入、排版选择器字段被劫持。已挪到双语
  字段之后,恢复 object+zh/en 结构(重复键检测:修前 4 处 → 修后 0)
- 新增 scripts/build-perf/config-check.py:用自定义 StrictLoader 在解析阶段
  对重复键报错(Python 默认 safe_load 静默覆盖是本次漏检根因)+ hint 覆盖
  率体检;build.sh 守卫挂载([ -f ] 先行,与既有闸门同级)

验证: 构建 48 页 exit 0;E2E 37/37;content-lint 49 页 0/0/0;对比度闸门
      4 项 PASS;config-check 五文件全 OK + 206 字段/64 hint

Spec: 2026-11-02_blog-5g-pages (T6)

Co-authored-by: GLM-5.3
2026-11-02 19:16:43 +08:00
wait4xandGLM-5.3 3e09a362cc refactor(home): 首页三字段默认值收敛到 SITE_DEFAULTS(5g T4 补)
- SITE_DEFAULTS 新增 homeProjectsLimit=6 / homeAnnouncement(空) /
  homePostsTracks(全开),访问器改为引用默认常量而非散落字面量
- 旧 yaml(无三字段)零回归实测:公告不渲染、项目墙默认 6 条、三轨全开

验证: 构建 48 页 exit 0;E2E 37/37(快照口径)

Spec: 2026-11-02_blog-5g-pages (T3/T4 补充)

Co-authored-by: GLM-5.3
2026-11-02 19:02:41 +08:00
wait4xandGLM-5.3 8debd99e61 feat(theme-home): 样式即数据 + 首页抽组件去重(5g T3/T4)
【T3 样式即数据】
- content/site/theme.yaml 九组白名单旋钮(accent 明暗两值 / radius_img /
  text_h1-h3 / prose_size_reading|tech / leading_reading|tech /
  space_3-6 / flow_p 与 flow_h2_post / dur);--measure、断点、纸墨、
  code 配色按方案 §3.6 红线不开放
- src/lib/theme.ts:getTheme() 钳制 + 单调性校验(h1>h2>h3、
  space3<4<5<6 任一不满足整组回落默认);themeVarsCss() 产出
  `:root:root{…}` 与 `[data-theme="dark"]:root:root{…}`,靠特异性压过
  tokens 的 :root,不依赖注入顺序
- BaseLayout head 内联 `<style is:inline set:html={themeStyle}>`:构建期
  静态、运行时零 JS 零请求
- scripts/build-perf/contrast-check.mjs 读产物断言正文/强调 × 亮暗四项
  ≥4.5:1,不达标 exit 1;build.sh 守卫挂载([ -f ] 守卫先行)
  实测:默认 12.40/10.53/6.30/7.06 全过;负例 accent #cccccc → 1.61:1 exit 1
  两坑入档:tokens 的 --bg 是 3 位简写需归一化;选 tokens CSS 要用
  `--bg:#` 定义断言(includes('--bg') 会误命中 var(--bg-soft) 引用)
- 后台 theme 单例 17 字段全中文 hint + 取值范围(分组约束写进 hint)

【T4 首页去重与细化】
- 新增 src/components/pages/HomePage.astro 承接全部区块(名片区富文本
  tagline/subtitle/社交、公告、文章列表、最新影像、热力图、项目墙、
  心跳),locale 一统;en 轨吃 postsEn 译题映射、图库链走 /en/gallery/,
  心跳仍仅 zh(零回归)
- 两 index.astro 缩成 4 行薄壳,双份漂移隐患消除
- home.yaml 三字段:projectsLimit(1-99 夹取,缺省 6)、announcement
  (双语 markdown 走 renderRich,空则不渲染)、postsTracks(三轨开关,
  在 HomePage 内过滤而非改 PostList,守边界)

验证: 构建 48 页 exit 0;contrast-check 四项 PASS;E2E 37/37(快照口径);
      实测 theme 变量内联、公告 markdown 粗体渲染、projectsLimit 限流、
      photo 轨关闭生效;验收数据已还原

Spec: 2026-11-02_blog-5g-pages (T3/T4)

Co-authored-by: GLM-5.3
2026-11-02 18:58:11 +08:00
wait4xandGLM-5.3 be87917443 chore(spec): 勾选已收编任务 T1/T2/T5/T7(T3/T4/T6 在途)
Spec: 2026-11-02_blog-5g-pages

Co-authored-by: GLM-5.3
2026-11-02 18:42:36 +08:00
wait4xandGLM-5.3 aaa9cd9274 feat(typography): 全站三层排版——富文本/页面级覆盖/艺术字体类
- 层 1 所有页面文案改富文本:site-content 新增 renderRich(md,{inline})
  复用既有全管线(raw→内容净化→class 白名单→链接卡片);接入
  home.tagline/subtitle、opensource.blurb、about.intro、blocks prose;
  新增 .rich 容器作用域并与 .prose-body 合并(77 处选择器覆盖四类容器)
  坑:renderRich 输出 <p> 塞进 <p class="tagline"> 会被浏览器拆散 →
  行内场景用 {inline:true} 剥外层 <p>
- 层 2 页面级排版覆盖:三单例加 typography{font?,accent?,scale?},读取器
  pageTypography() 走白名单(10 字族 / 18 色类,scale 夹 0.8–1.25,清单外
  忽略);页面容器挂 .page-typo + CSS 变量 --page-font/--page-accent/
  --page-scale,不动全站 tokens;后台三页加「页面排版覆盖」下拉
- 层 3 艺术字体:新增 font-art-kuaile(快乐体专用类,字距微调),白名单与
  可视化面板同步;站酷高端黑/阿里妈妈东方大楷/演示悠然小楷因无 npm 包或
  嵌入条款风险进候补清单
- 默认值清理:about.yaml 的 typography 测试值(整页汇文)清为注释示例——
  整页换字族会带来 694KB 字体成本,默认留空=全站默认排版

验证: 构建 48 页 exit 0;E2E 37/37;三页 page-typo 挂载与变量实测、四页
      (图库/书架/文章/装备)隔离为 0、font-art-kuaile 生效、about 清空后
      无覆盖

Spec: none (排版三层,先于 5g 的任务;T3/T4 另行交付)

Co-authored-by: GLM-5.3
2026-11-02 18:40:32 +08:00
wait4xandGLM-5.3 2f7cbcf7bd feat(about): 块式页面构建器——九类区块 + 三级回落零回归(5g T1/T2)
- schema:siteAbout 增 layout{showIdCard} + blocks(z.discriminatedUnion
  九类:prose/tracks/cards/timeline/quote/figure/linklist/cta/divider,
  图片字段走 Astro image());对未知块型用 z.union([knownUnion,
  z.looseObject({type})]) 复合——已知块严格校验,未知块落宽松分支再由
  访问器 WARN+丢弃(容错与严格校验的张力取舍)
- page-blocks.ts:enabled:false 过滤、未知 type WARN+skip、pickBi 回落
  zh 并 WARN、上限(块≤24/单块文本≤4000,超限截断并 WARN)
- BlockRenderer 统一外壳 `<section class="blk" data-block={type}>`(E2E
  抓手)+ 九个块组件(样式自 AboutPage 原 <style> 搬迁,只吃既有 token)
- AboutPage 三级回落:blocks → intro markdown → 内置默认;legacy
  tracks/build 段整体挪入回退分支(文案逐字一致,零回归)
- 图标白名单 src/lib/icons.ts(12 键,未登记回退首字占位)
- 后台 about 单例四段结构(blocks/layout/intro/typography),块为
  list+types+typeKey,每块带「显示」开关与中文标签

协调报备:车道修了一处跨车道插入事故(T3 把 typography 插进了 intro
的 widget 内导致字段错位),重写为四段正确结构;并发构建擦 dist 的
假失败改用「build→快照 dist→快照起服务跑 E2E」规避(37/37 稳定)

验证: 构建 48 页 exit 0;E2E 37/37(快照口径);九类块顺序与配置一致、
      figure 走 /_astro/*.webp 管线、未知块与外链图被拦并 WARN、
      终版 3 块零回归、en 同构、admin about 四段字段齐全

Spec: 2026-11-02_blog-5g-pages (T1/T2)

Co-authored-by: GLM-5.3
2026-11-02 18:21:33 +08:00
wait4xandGLM-5.3 6096a9dafe feat(opensource): README 四级回落 + 兜底 JSON 8 仓 + 项目可见性告警
- site-content.ts:README 取数加 AbortSignal.timeout(10s)(原无超时,
  慢端点可卡整条构建);四级回落矩阵逐级 console.warn 点名原因:
  新鲜网络(内容有变才回写缓存)→ token 缺位/HTTP 非 2xx/空/超时/异常
  时吃缓存 → 缓存亦缺回落 yaml blurb → 缓存回写失败(构建仓只读)仅告警
- README 图片加固:markdown 层给每个 img 补 loading=lazy + 缺省 alt
  (幂等;只动这两个属性,因为过 rehype-content-sanitize 白名单后其余
  属性会被剥);相对链接不重写,限制写入 JSDoc 与文档
- projects.ts:单源失败点名、双源全失败点名回落 JSON、mode=only 的
  include 未命中双向点名(双源无此仓 / 被 perSource 过滤掉)
- projects-fallback.json:1 → 8 条真实仓(blog / open-earth-data-guide★58
  / AI-CLI / open-earth-data-kit / pi-mesh / pi-board / pi-jobd /
  ai-code-style-kit,字段对齐 RepoCard)

车道:worker-one(T5);收编复验:E2E 37/37、构建 48 页 0 ERROR、
兜底 JSON 8 条断言过

验证: E2E 37/37 PASS;pnpm astro build 48 页 exit 0;fallback 8 条

Spec: 2026-11-02_blog-5g-pages (T5)

Co-authored-by: GLM-5.3
2026-11-02 18:13:47 +08:00
wait4xandGLM-5.3 59eda420b3 perf(fonts): 字体子集化工具 + 汇文明朝单页 -40%(694KB→420KB)
- 新增 scripts/build-perf/subset-fonts.py:读字体 CSS 的 @font-face
  (unicode-range)→ 扫内容收集实际用字 → 逐片交集裁切(pyftsubset
  --flavor=woff2)→ 空片丢弃、重写 src 只留自产 woff2;支持
  --chars-from/--chars-mode(all|headings)/--min-bytes/--merge
- 实测口径修正后结论(关键):汇文明朝原 694KB/页 → 子集 420KB/页
  (-40%,真收益,已接入);站酷快乐体 163KB → 177KB(+9%,字跨片
  分散致片数增加,**不启用**,保留原 @fontsource 源)
- merge 模式(合并单文件)标记实验性:fontTools.merge 对 CJK 切片字表
  冲突敏感,实测产出异常,生产不用
- 三处实现坑入档:①hast 与 @fontsource CSS 一个 face 常带多格式
  (woff2+woff),只替换首个 url 会残留失效引用(首版缺 58 个 .woff)
  → 改整段替换 src;②无 unicode-range 的 face 需按全范围处理,否则
  原样保留同样残留引用;③裸跑 pyftsubset 需 fonttools+brotli

验证: pnpm astro build 48 页 exit 0;E2E 37/37;playwright 实测汇文
      694KB→420KB 且字体栈生效(WF-Hwmc 命中)

Spec: 2026-11-02_blog-5g-pages (T7)

Co-authored-by: GLM-5.3
2026-11-02 18:13:47 +08:00
wait4xandGLM-5.3 740ec470fa chore(spec): 立项 blog-5g——页面后台化(blocks)与字体子集化
按 docs/research/10 方案 E 拆七任务:块式页面构建器(T1/T2)、样式即数据
+对比度闸门(T3)、首页抽 HomePage 去双份(T4)、开源页收尾(T5)、文档
与 hint(T6)、字体子集化工具(T7)。三车道分配:op-gitea(T1/T2)、
op-artalk(T3/T4)、worker-one(T5/T6)、orchestrator(T7+统筹验收)。

Spec: 2026-11-02_blog-5g-pages

Co-authored-by: GLM-5.3
2026-11-02 17:50:19 +08:00
wait4xandGLM-5.3 3822e150a8 feat(typography): 首批 6 字族落地(新增汇文明朝/站酷快乐)+ 13 款候补清单
- 新增两款自托管字族:font-huiwen(汇文明朝体,文风开源 cn-font-split
  339 片切片包,用到页 +535KB、按需拉取实测 19/339 片)|font-kuaile
  (站酷快乐体 +163KB)——补齐「宋明气质」与「轻松标题」两个字体缺口
- 首批 6 族全部 OFL-1.1 且允许 web 嵌入;未用字体的页面增量恒 +0.0KB
  (按页子集化生效)
- 候补 13 款逐款标注「许可证 + web 嵌入」:HarmonyOS Sans/MiSans/OPPO
  Sans/阿里普惠体(免费商用 + 明确允许嵌入,但 npm 无包/整包过大)、
  得意黑/更纱/站酷高端黑/花园明朝(OFL 但无可用 npm 分发)、霞鹜文楷
  Light(+8605KB 否决)、演示悠然小楷("个人免费"常禁 @font-face,风险)
- docs 写入「加字四步流程」(核双授权 → 装包或放 public/fonts → 加类+
  白名单+面板项 → 跑体积账)
- 可视化面板 35 项(10 字族 + 25 颜色);白名单放行 10 字族类
- 资产:public/fonts/hwmc(339 woff2 13MB + fonts.css 162KB)入仓

车道:op-artalk;收编复验:构建 48 页、E2E 37/37、两款新字体 computed
与切片按需加载实测(hwmc/27、52 等 20 个请求)

验证: pnpm astro build 48 页 exit 0;E2E 37/37;playwright 新字体 computed
      + 字体请求观测

Spec: none (字体扩展,车道级交付)

Co-authored-by: GLM-5.3
2026-11-02 17:47:13 +08:00
wait4xandGLM-5.3 26bbb66a72 feat(typography): 16 类语义色板(8 色相×2 档)+ 字体回退升级 + 面板 v2
- 颜色:c-{blue,teal,green,yellow,orange,red,pink,purple} × {标准,soft}
  共 16 类;tokens 明暗各一套值,32 项浏览器实测对比度 4.50–11.73(零失败);
  黄色压到 #7a5900(原色白底仅 ~1.8:1)、黄/粉 soft 压深留余量
- 使用纪律入档:彩色只作用于正文强调,站点外壳(导航/页脚/按钮/背景/边框)
  保持灰阶,单页彩色强调建议 ≤3 处
- 字体:3/4 回退类升级为「自托管优先 + 系统兜底」(font-song→Noto Serif SC、
  font-kai-sys→霞鹜文楷、font-hei-sys→Noto Sans SC,+0.0KB 跨设备一致);
  仿宋无 OFL 自托管包(fandol/朱雀仿宋均无版本)→ 纯系统回退,缺口待决策
- webfont 候选全部按体积账否决并留档:霞鹜文楷 Light 用到页 +8605KB、站酷小薇
  +648KB、得意黑/更纱/汇文无可用 npm 包;根因=@fontsource CJK 切片粒度
- 可视化面板 v2:33 项(8 字族 + 25 颜色),颜色项带 16px 色块预览、字体项带
  示例字形「秋海」并标注自托管/优先/系统;桩 CMS 7 项验证全过
- 白名单放行 25 色类 + 8 字族类;docs 三表(灰阶层级/语义色板含对比度/字体
  含许可证与体积账)更新

车道:op-artalk(改口径后重跑,实测表齐全);收编复验:CSS 16 色类定义与
白名单一致、暗态值在 [data-theme="dark"] 段、字体回退类 computed 生效、
构建 48 页、E2E 37/37

验证: pnpm astro build 48 页 exit 0;E2E 37/37;playwright 抽查彩色类与
      字体回退 computed

Spec: none (排版色板与字体回退,车道级交付)

Co-authored-by: GLM-5.3
2026-11-02 17:14:12 +08:00
wait4xandGLM-5.3 2cab243891 feat(typography): 可视化排版选择器(Sveltia inline 组件)+ 颜色扩充 + 方案 10
- 可视化选择(核心痛点解决):Sveltia 原生支持 registerEditorComponent
  的 inline 形态且 onInsert 能拿到 selectedText → 实现「选中文字 → Insert ▸
  排版 → 选字体/颜色 → 自动包 span class」完整链路;因 inline 组件只能进
  Insert 菜单(field-based 虽可上工具栏但拿不到 selectedText),两个独立组件
  会互相抢 pattern 匹配,故收敛为单入口 + 18 项分组面板(键盘可达/Esc 取消/
  onEdit 回填当前类)
- public/admin/typography-classes.js 为单一事实源(扩展与文档共用)
- 颜色扩充 4 类:ink-warm / ink-cool / ink-faint / ink-accent-soft,全部亮暗
  双态实测 ≥4.5:1(ink-faint 取白底达标的最淡值 4.69/4.75)
- 字体:体积账否决 webfont 新增(@fontsource zcool-xiaowei 用到即 +647.7KB,
  远超 150KB 阈值;smiley-sans 无可用版本、sarasa 包为空壳),改为落地 4 个
  **系统回退类**(font-song/font-kai-sys/font-hei-sys/font-fangsong,零下载,
  文档注明依赖访客设备)
- 白名单 17 类同步;docs/写作指南.md 三表更新
- docs/research/10-页面后台化方案.md:五方案对比 + 推荐方案 E(页面骨架 blocks
  / 区块字段 / 局部自由区 / 样式即数据四层)+ 关于页九类区块 + 消毒与验收断言
  + 分阶段清单(约 7.2d)

验证: 16/16 颜色 computed 与对比度实测;选择器 7/7(桩 CMS 覆盖注册/插入/
      编辑三层);构建 48 页 exit 0;E2E 37/37

Spec: none (排版能力 + 方案文档)

Co-authored-by: GLM-5.3
2026-11-02 16:57:54 +08:00
wait4xandGLM-5.3 c5cdcd5bf9 feat(opensource): README 开篇接线+缓存入仓(收编遗漏补齐)
zh 开源页补收编(此前只提交了 en):githubReadmeMarkdown + renderMarkdown
接线,content/.github-readme.md 缓存入仓(现状 intro_from=off,开启即生效)。

验证: 构建 48 页 exit 0

Spec: none (收编遗漏补齐)

Co-authored-by: GLM-5.3
2026-11-02 16:57:53 +08:00
wait4xandGLM-5.3 b0a3881725 fix(security): 页面文案 markdown 管线加标签/属性/协议净化(P0)
方案 10 §7.1 实锤漏洞:renderMarkdown() 用无参 createMarkdownProcessor,
默认 allowDangerousHtml + 无条件 rehype-raw,页面文案(about intro、开源页
叙述、README)的 raw HTML 直通 set:html——script/style/iframe/on* 全不剥。

- 新增 src/lib/rehype-content-sanitize.ts:标签白名单(不含 script/style/
  iframe/object/form/link/meta/svg 等,危险标签整棵丢弃、未知标签解包保内容)
  + 属性白名单(a[href,title,rel]、img[src,alt,width,height,loading]、class/id)
  + 协议白名单(http/https/mailto + 站内相对 + 小图 data:)
- site-content.ts 插件链与文章管线同序:rehypeRaw → 内容净化 → class 白名单
  → 链接卡片(副作用为正:页面文案里的独立链接也会成 OG 卡片)
- 修两处自身实现坑(实测驱动):hast 的 class 属性名是 className(按 class
  过滤会把合法排版类一起剃掉);属性白名单需统一小写比较(驼峰 key 比不到)

验证: 注入载荷实测(<script>/<iframe>/onclick/style="color:red"/
      href="javascript:")全部剥离、合法 class="ink-2" 与文本保留;
      构建 48 页 exit 0;E2E 37/37

Spec: none (安全修复,方案 P0)

Co-authored-by: GLM-5.3
2026-11-02 16:57:52 +08:00
wait4xandGLM-5.3 859cac19a2 feat(cms): 首页四项可配 + 开源页项目可见性 + GitHub README 开篇
- 首页:subtitle(markdown 短文本)|latestPhotos 0-6(0 隐藏)|
  perTrackLimit 4-8|sections{tracks,photos,heatmap,projects} 开关,
  访问器 homeConfig() 夹取越界值,字段级回落默认(零配置=原形态)
- 开源页项目可见性:mode all|only|exclude + include[]/exclude[] +
  sort pinned-first|stars|updated + perSource both|github|gitea;
  projects.ts 加 LoadProjectsOptions + arrange()(来源→可见性→排序)
- 开源页开篇支持 GitHub README:intro_from off|github-readme,构建期
  取 wait4xx/wait4xx README(token 复用宿主 0600 文件)→ 缓存
  content/.github-readme.md 入仓 → renderMarkdown;失败逐级回落
  (缓存 → yaml blurb)。实测取到 3117 字符,现状仍 off 待用户开启
- 修既有 bug:orderPinned 内硬 slice(0,6) 把开源页 limit=99 也截到 6 条
  → 移除,开源页恢复全量 16 条
- 文档:写作指南加首页/项目墙两张字段表(含 YAML 1.1 `off` 需加引号坑、
  README 行为与外部徽章风险说明)

车道:op-gitea(3/3);收编复验:构建 48 页 + E2E 37/37 + 开源页 16 条 +
首页默认形态(2 轨/热力图/项目墙)无回归

验证: node v26.13.0 构建 48 页 exit 0;E2E 37/37 PASS;playwright 实测

Spec: none (首页与开源页后台扩展,车道级交付)

Co-authored-by: GLM-5.3
2026-11-02 16:45:01 +08:00
wait4xandGLM-5.3 8d221ab36e fix(home): 分轨标题贴左——space-between 把中间项挤到正中
分轨标题行有三个 flex 子项(::before 灰点 + 「阅读/技术」文本 + 右侧
「全部文章 →」),justify-content: space-between 会把中间的标题文本
挤到正中(用户实测"阅读/技术居中了")。改为 flex-start 贴左 + 链接
margin-left:auto 右推。实测标题距容器左缘 18px(灰点+间距),链接贴右。

验证: pnpm astro build 46 页 exit 0;playwright 实测两段标题 titleLeft=18
      linkRight=0

Spec: none (单点样式修复)

Co-authored-by: GLM-5.3
2026-11-02 16:39:11 +08:00
wait4xandGLM-5.3 c4bdf46614 fix(ops): .mjs MIME 修复脚本——pdf.js worker 线上加载失败根因
线上 /_astro/pdf.worker.min.*.mjs 被 nginx 以 application/octet-stream
返回(mime.types 无 mjs 映射),浏览器拒绝以 module worker 加载 →
/papers/ 页 pdf.js 静默回落兜底卡;本地 python 测试床能猜对 MIME 故未
暴露。脚本幂等追加 mjs 到既有 javascript 映射行,-t 门禁+回滚+线上终验。

验证: bash -n 过;线上 worker 头实测 application/octet-stream(病根实证)

Spec: none (运维修复脚本)

Co-authored-by: GLM-5.3
2026-11-02 16:03:30 +08:00
wait4xandGLM-5.3 280654f9e3 feat(reader): PDF 改 pdf.js 渲染+斜体 bug 根治+评论假挂载修复+排版类
【T1 PDF 内置阅读重做】iframe 对 arXiv PDF 实测 ERR_ABORTED(浏览器 PDF
插件策略差异)→ pdfjs-dist 6.4.299 客户端 canvas 渲染:只在 /papers/
动态 import(worker ?url 打包,不进全站 bundle);文档一次取回 + IO
懒渲染(rootMargin 400px)+ 页占位 aspect-ratio 防跳动 + 12s 首屏兜底 +
微信 UA 直接兜底;工具条页码/缩放/翻页/下载/arXiv/授权声明保留。红线:
PDF 由访客浏览器直取 arXiv(CORS * 实测),仓内零副本。真页实测:
13 页 canvas 全绘制、首屏墨迹像素非空、零 console 错误。

【T2 斜体排版 bug 真凶】global.css 的 `.prose-* p > em:only-child
{ display:block; text-align:center }` 图注样式按"斜体独占段"自动命中,
把所有正文独占段斜体变成块级居中。改:图注用显式 .fig-cap 驱动 + 补
em/i { display:inline; font-style:italic } 基线;media-sample 迁移新写法,
指南注明旧写法失效原因。实测独占段与行内斜体均 display:inline/italic。

【T3 评论"假挂载"真 bug】原 initArtalk 一进来就置 mounted=true——脚本
404/被拦截时把空容器误标已挂载,5s 兜底被绕过,正是"评论区一直空着"的
来源之一。改为库就绪才算成功;另加三触发路径(IO 主 + 锚点点击 + 滚到
页底 2s 兜底)、5s 失败提示+重试钮、软导航重挂、成功打
data-artalk-mounted=1 供 E2E。

【T4 文章排版类 + 安全白名单】11 个受限类(ink-1/2/3/muted/accent、
font-serif/kai/sans/mono、drop-cap、quote-line、fig-cap)全走 tokens 禁
任意 hex;新增 rehype-class-whitelist.ts:className 白名单 + 剥离 style
与 on*;实测注入用例只剩 class="ink-2",style/onclick 全无。新坑入档:
markdown raw HTML 在 hast 是 type:'raw' 节点、白名单插件看不见 span,必须
先 rehype-raw 再过滤 → 插件链 [rehypeRaw, rehypeClassWhitelist,
rehypeLinkCards]。新依赖:pdfjs-dist、rehype-raw。

车道:op-artalk(T1 6/6、T2 2/2、T3 3/3、T4 6/6);收编复验:build 46 页、
E2E 37/37、四专项 playwright 实测(PDF 13 canvas/斜体 inline/白名单净化/
评论 mounted 标记)

验证: node v26.13.0 + astro 7.4.4 构建 46 页 exit 0;E2E 37/37 PASS

Spec: none (阅读体验四件,车道级交付)

Co-authored-by: GLM-5.3
2026-11-02 15:59:28 +08:00
wait4xandGLM-5.3 328ab68f18 feat(cms): 后台扩展五件——关于页 markdown 化/社交/头像/favicon/分轨首页
- T1 关于页=markdown 文档:about.yaml intro 扩为多段 markdown(zh/en),
  AboutPage 用 @astrojs/markdown-remark 的 createMarkdownProcessor(进程
  内单例复用)+ set:html 渲染,正文 p/ul/strong/a 对齐站点排版;名片与
  技能卡结构不动
- T2 社交链接单一数据源:home.yaml socials[](label/url/icon)+
  SocialLinks.astro(图标注册表白名单:github/rss/email/bilibili/x,
  未登记 icon 回退纯文字),首页 zh/en 内联硬编码社交块删除
- T3 头像与 favicon 可上传:schema avatar: image()(Astro Image 出 60px
  WebP,未设回退渐变 SVG);home.yaml favicon → BaseLayout link[rel=icon]
  按扩展名给 type,留空回退 /favicon.svg
- T4 首页分轨:PostList perTrack → 阅读轨/技术轨两段各限 6,各自出口
  (阅读→/books/ 技术→/archives/,en 同构);E2E 断言改为两段各≤6+各自
  出口+阅读在前
- T5 后台列表日期:posts summary「{{pubDate}} · {{title}}」、moments
  「{{fields.pubDate}} · {{fields.text}}」+sortable;photos 无 frontmatter
  日期(JSON sidecar)保持分组 · 标题并在文档写明原因
- 顺手修 content-lint 漏报:中文文件名在 HTML 里为 %XX 编码,脚本直接
  拼盘上路径导致 6 条"缺图"误报 → 加 decodeURI
- 清理:删除书封误传遗留的双重嵌套目录文件

车道:op-gitea(5/5,build+E2E 37/37+lint 47 页 0/0/0);收编复验:
build 46 页 + E2E 37/37 连跑(首跑两红为车道并发构建擦 dist 的瞬时态,
复跑绿)

验证: node v26.13.0 + astro 7.4.4 全量构建 46 页 exit 0;E2E 37/37;
      content-lint 0/0/0;books details/命令面板 playwright 实测

Spec: none (后台能力扩展,车道级交付)

Co-authored-by: GLM-5.3
2026-11-02 15:46:41 +08:00
wait4xandGLM-5.3 227bc24797 build(deps): Node 26.13.0 + Astro 7.4.4 升级(含 exif loader 句柄修复)
- 宿主 Node 22.22.3 → 用户级安装 26.13.0(~/.local/node),systemd
  unit PATH 前置,CI 构建链同步吃到新版本
- Astro 7.3.8 → 7.4.4(7.4 要求 Node ≥22.23.4,此前升级因 Node 版本
  不足被 CI 拒绝;本次先升 Node 再升框架)
- exif loader 修复:Node 22.23+/26 把"GC 时仍打开的 FileHandle"从警告
  升级为错误,exifr 接收文件路径时自开 fd 不显式关闭 → 全线 content/
  photos/*.jpg 触发构建失败。改为一律 readFile 成 Buffer 再喂 exifr
  (无 fd 参与)

验证: 全量构建 44 页 exit 0(node v26.13.0 + astro 7.4.4);E2E 37/37;
      content-lint 0/0/0;后处理链(字体子集/首页内联)复跑通过;
      unit PATH 实测已前置 node26

Spec: none (运行时与框架升级,全链验收)

Co-authored-by: GLM-5.3
2026-11-02 15:27:28 +08:00
wait4xandGLM-5.3 7c1462bde0 fix(admin): 书封上传路径修复——media_folder 相对解析 + Astro 暂留 7.3.8
- Sveltia 对 folder 集合的 media_folder 按"相对集合目录"解析:写成
  content/posts/assets 会二次嵌套成 content/posts/content/posts/assets
  (用户在后台首次上传书封即踩,文件落错位置 + cover 引用缺失 →
  构建 ImageNotFound,发布链中断)。修为 assets,并把已落错位置的
  书封文件移到 content/posts/assets/
- Astro 7.4.4 需要 Node >= 22.23.4,宿主为 22.22.3:升级将锁死 CI
  构建(实测 CI 报 not supported)。本笔回落到 7.3.8 恢复发布链;
  Node 升级后另行升级 Astro(含全链验收)

验证: pnpm astro build 44 页 exit 0;书封图片入 content/posts/assets/
      与文章 cover 路径一致

Spec: none (后台路径修复 + 依赖回滚)

Co-authored-by: GLM-5.3
2026-11-02 15:09:03 +08:00
wait4x 5c814f0866 Update 页面文案 “opensource” 2026-11-02 06:59:40 +00:00
wait4xandGLM-5.3 8f01d825b7 build(deps): Astro 7.3.8 → 7.4.4(同 major 小版本升级)
- 项目 Phase 1 曾故意锁 7.3.8(Astro 7 换 Rust 版 Sätteri Markdown
  处理器,自定义插件压在 legacy unified 路径上)——现 D 车道已装
  @astrojs/markdown-remark 兜住该路径,升级窗口成熟
- 7.4.x 对本站有实际收益的三项修复:①<Image>/<Picture> 重复 srcset
  候选(图库/书封/OG 卡缩略图大量使用)②ClientRouter 返回导航不再播放
  smooth 滚动动画(本站开 View Transitions)③未使用组件 scoped 样式
  泄漏(与 CSS 体积优化相关)
- 升级后清 node_modules/.astro(渲染缓存)全量重建

验证: 全量构建 45 页 exit 0;E2E 37/37 PASS;content-lint 0/0/0;
      后处理链复跑(字体子集 8.0→3.2MB 60% 裁减、首页 CSS 内联、
      SW 预缓存 19 路由);回归抽查(图库 srcset 零重复候选=7.4 修复
      实证、灯箱开合、g s 弦、链接卡片渲染)全过

Spec: none (依赖小版本升级,全链验收)

Co-authored-by: GLM-5.3
2026-11-02 14:58:35 +08:00
wait4x f8562510e6 Update 文章 “hai-feng-zhong-shi-luo-de-xie-se-kui-zeng” +1 2026-11-02 06:55:03 +00:00
wait4x 952e68e6ea Delete 瞬间 “2026-04-29-2” +1 2026-11-02 06:51:59 +00:00
wait4xandGLM-5.3 46d248877f fix(opensource): zh 开源页补 pinned 参数接线(收编遗漏)
内容管理车道改了 zh/en 双页,收编时只圈了 en 版,zh 版靠默认
PINNED_DEFAULT 兜底(行为等价但接线不一致)。补收编。

验证: pnpm astro build 45 页;E2E 37/37;content-lint 0/0/0

Spec: none (收编遗漏补齐)

Co-authored-by: GLM-5.3
2026-11-02 12:50:32 +08:00
wait4xandGLM-5.3 f5232b7c03 feat(content): 链接卡片模式——独立成段裸链接转 OG 预览卡
- rehype-link-cards.ts 构建期插件:段落仅含一个链接才转卡(内联链接
  保持普通样式);fetch OG/twitter 元信息(UA 伪装、4s 超时、只读前
  200KB)+ favicon 兜底;任何失败回落普通链接,构建绝不因外部站点挂
- content/.link-cache.json 入仓缓存(30 天刷新,构建确定性与零重复
  请求);单篇上限 10 卡、并发 4,超出回落
- LinkCardStyles.astro 灰阶卡片样式:hairline+圆角、favicon 16px、
  标题一行/描述两行截断、域名 mono、OG 图右侧 88px(无图不占位)、
  hover 1px 微浮(reduced-motion 豁免)、纯 CSS 零 JS
- 安全:抓取内容全 escaping、图片地址仅 http(s)
- @astrojs/markdown-remark 依赖(Astro 7 换默认 markdown 处理器后
  自定义插件必须经此包生效)
- 样张 content/posts/link-cards-test.md(tags 含 hidden,活样例不列出)

修复说明:264148f 提交时误带入 archives 页面对 LinkCardStyles 的
import(两车道共改同一文件),本笔补全组件与插件,恢复构建仓可用。
教训:跨车道共改文件,staged 前必须看 diff 归属。

车道:op-artalk;收编复验:本地构建 45 页 + 卡片渲染(后续专项复验)

验证: pnpm astro build 45 页 exit 0;link-cards 样张页渲染正常

Spec: none (链接卡片,车道级交付)

Co-authored-by: GLM-5.3
2026-11-02 12:46:26 +08:00
wait4xandGLM-5.3 264148f0a6 feat(cms): 内容管理闭环——站点单例三件套+文章视频字段
- A 站点文案单例:content/site/{home,about,opensource}.yaml +
  src/lib/site-content.ts 访问层(字段级回落默认,默认值=原文案逐字
  一致,零回归);接线主页 tagline(en 首页新增该块)、关于页 intro、
  开源页叙述三段 + PINNED 参数化(projects.ts: loadProjects(pinned)
  /orderPinned,PINNED_DEFAULT 仅缺省兜底)
- loader 语义修正(返工如实入档):Astro 7 的 file() 把 YAML 顶层对象
  按 Object.entries 拆成多条目(id=key、data=value),单例字段必炸
  schema;改 glob 精确到单文件 = 真单例语义,data 即文件根对象,
  Sveltia 可编辑根字段零嵌套(比包 main: 层的方案后台体验更好)
- B 文章媒体:posts +video/+poster;Sveltia file 字段
  (media_folder=public/media、public_folder=/media、hint ≤50MB);
  正文后渲染 video controls preload=metadata playsinline,无则不渲染;
  图片链路核对本已一致(./assets 走 Astro 管线);链接 markdown 原生
- docs/写作指南.md 补「后台上传」节(三类媒体 + 三单例操作 + 体积纪律)

Sveltia 集合:posts / moments / photos / site_pages(三单例)

车道:op-gitea(实测 tagline 改 yaml 产物联动、置空回落默认、pinned
改名排序变、视频临时件渲染全对、构建绿 + lint 45 页 0/0/0);收编
复验:构建 44 页 + E2E 37/37 + 三 yaml 扁平键就位

验证: pnpm astro build 44 页 exit 0;E2E 37/37 PASS;content-lint
      0/0/0;播放器/单例/置顶 playwright 实测全过

Spec: none (内容管理闭环,车道级交付)

Co-authored-by: GLM-5.3
2026-11-02 12:42:10 +08:00
wait4xandGLM-5.3 5be09b6f32 fix(admin): 后台后端改博客域同源反代——绕开 git 域 522
git.wait4x.online 公网 522(源站侧待查)卡死 Sveltia 登录。
config.yml backend 改 base_url=https://wait4x.online/gitea +
api_root=.../api/v1;新增运维脚本给博客 vhost 加 /gitea/ 反代
→127.0.0.1:13020(Host 伪 git 域、no-store、noindex;幂等+-t 门
禁+回滚+双口径终验)。同源后 CORS 问题一并消失。

验证: yaml 解析过;pnpm astro build;bash -n 过

Spec: none (后台可用性修复)

Co-authored-by: GLM-5.3
2026-11-02 11:18:35 +08:00
wait4xandGLM-5.3 de42fed52e fix(ops): /admin/ 404 修复脚本——location 块补 root
病根:博客 vhost 的 root 写在 location / 内(launch.sh 静态切换设
计),nginx 的 location 不向上继承 root,故后来加的 location
/admin/ 块落到默认根目录 → 404(用户打不开后台的直接原因)。
脚本幂等重写 admin 块(root+index+noindex 局部),-t 门禁+回滚+
双口径终验。

验证: bash -n 语法过;本地 dist/admin 三件齐全(config/index/js)

Spec: none (运维修复脚本)

Co-authored-by: GLM-5.3
2026-11-02 11:13:09 +08:00
wait4xandGLM-5.3 845c45aa11 feat(admin): Sveltia 加 posts 集合——文章与书封纳入后台管理
内容管理闭环补齐:文章(三轨,书=reading 轨同集合)可在后台新建
/编辑/上传书封;media 指 content/posts/assets(public ./assets,书封
上传即自动写路径);含标题/日期/分轨/标签/情绪色/原书作者/策展
描述/书封/隐藏/草稿/正文全字段。英文仍走 TMT 预翻译入仓,不进
后台防误改译文。

验证: yaml 解析三集合 posts/moments/photos;pnpm astro build 通过
      (admin 静态产物含新 config)

Spec: none (后台集合扩展)

Co-authored-by: GLM-5.3
2026-11-02 11:10:00 +08:00
wait4xandGLM-5.3 0e06c25f67 feat(gallery): 灯箱 v2.2 定版——一律侧栏 200px+长文策略+六条 polish
布局统一(视觉轨像素实测裁定 A 改窄变体:侧栏方案让照片比底
栏方案大 39%/22%):≥820 全比例右侧栏 200px(舞台恰 1080×720
零信箱)、竖横顶部统一 26px;<820 底部抽屉。EXIF 两行折行+分段
span(字段级 nowrap+::before 绑后段符号——一刀治悬空/断点丢符/
间距不对称三症状);caption clamp 4 行+展开(桌面 60dvh/移动
40dvh 内滚,锁 --fig-h 保证展开前后图区几何不变);移动底条
flex 复活+箭头同排+EXIF 两行;提示胶囊移动挂顶+触屏文案;820
计数间隙 14px;全量左对齐(孤字居中终止)。

车道:op-artalk(专项 8/8×3 轮+E2E 37/37);视觉轨道三轮裁定
(需修→复检→定版),EXIF 间距自验 5.2px 对称、换行对 1(镜头
名独占行,非缺陷)

验证: E2E 37/37;EXIF 三段宽 measured(5.2px 对称/符号 ::before
/无悬空);移动 flex+提示顶挂实测;五档几何(1280 横竖/820/768/390)

Spec: none (灯箱 v2.2 定版,车道级交付)

Co-authored-by: GLM-5.3
2026-11-02 10:30:35 +08:00
wait4xandGLM-5.3 3b951d3de6 feat(archive): 文章折叠——首页 6 篇+归档年月折叠+书架年折叠
①首页 PostList limit=6 + 「全部文章 →」双轨入口(用户拍板 6 篇)
②/archives 年 <details>(最新年 open,summary「2026 · N 篇」+▸ 自绘
箭头)+ 年内按月分组小标题;零 JS 依赖、内容全在 DOM(SEO
无损)③/books 年折叠(色脊/书封渲染零改动)④E2E +3 断言
(首页≤6+入口/归档 open 唯一+月分组/书架年组不变量)

E2E 弦键竞态根治:折叠批的多次导航扰动 g-chords 时序——洁净起
点(先回稳定页)再测弦,测试意图不依赖前序状态,37/37 连跑稳定

车道:op-gitea(lint 43 页 0/0/0);收编复验首页 6 行+入口/归档
1 年开 12 月组/书架年折叠/en 轨全过

验证: E2E 37/37 连跑两次;content-lint 0/0/0;首页/归档/书架/
en 四页 playwright 实测

Spec: none (折叠批,车道级交付)

Co-authored-by: GLM-5.3
2026-11-02 10:00:45 +08:00
wait4xandGLM-5.3 9702c9faf4 feat(books): 书封接口——schema cover 字段+书架卡封面渲染
posts schema 加 cover?: image(image() 走 Astro 优化管线,仅阅读
轨消费);BooksPage 有封面渲染 96/192 两档 Image、无封面优雅回
退现有色脊卡设计(零破坏);目录 content/posts/assets/books/ 就位;
docs/写作指南.md 补用法节(路径/frontmatter 示例/尺寸建议)。

验证: pnpm astro build;E2E 34/34;现 5 本书全部回退色脊卡渲染
(封面貌 0,待用户加图)

Spec: none (书封接口预留,用户自行加图)

Co-authored-by: GLM-5.3
2026-11-02 09:12:34 +08:00
wait4xandGLM-5.3 2e33a7cd2e fix(pwa): SW 缓存版本 v2→v3——冲刷主题修复前的旧页缓存
用户报首页刷新偶现浅色、内页深色。本地/生产双端复测均正确
(首帧即 dark),最可能是 SW 缓存里残留 data-astro-rerun 修复前的
旧首页(无主题脚本)。版本号 bump 强制 activate 清旧缓存。

验证: pnpm astro build;本地四页帧序列实测全 dark

Spec: none (缓存冲刷,环境性症状防御)

Co-authored-by: GLM-5.3
2026-11-02 09:06:42 +08:00
wait4xandGLM-5.3 3050b0b959 feat(article): 正文图片可放大——PhotoZoom 极简 viewer 双轨挂载
正文 .prose-reading/.prose-tech 图片单击唤起全屏 dialog:fit-
contain+滚轮光标锚点缩放 1-5x+拖拽(钳制数学与图库 v2 逐行同
源)+双击 1x↔2.5x+Esc/关闭/背景点击关(contain 内容盒几何判
定);无 EXIF/caption/FLIP(纯图极简,按契约);document
委托点击软导航后仍生效。

车道:op-artalk(专项 9/9:SVG 架构图唤起/矢量 2.46x 不栅格化/
拖拽钳制/双击回正/Esc/jpg/背景关/软导航);收编验收四断言+E2E
34/34 全过

验证: 文章 SVG 点击唤起+滚轮放大(naturalWidth 960 矢量源)+Esc
      关闭实测;E2E 34/34

Spec: none (文章图放大,车道级交付)

Co-authored-by: GLM-5.3
2026-11-02 08:47:24 +08:00
wait4xandGLM-5.3 a761cf8129 feat(papers): 论文内置阅读——arXiv iframe 零再分发合规方案
Agensh(arXiv 2609.26781)在线研读页:/papers/agensh/ 双轨薄壳
_PaperView 共享视图+六词条;信息条含标题/作者/abs 链接/授权声明
(arXiv nonexclusive-distrib 1.0,非本站托管)/下载钮。

合规红线:许可证禁第三方再分发→仓内零 PDF 副本,iframe 直嵌
arxiv(实测无 X-Frame-Options);微信 webview 前置 UA 检测加
is-wechat 类、iframe 不挂 src 零无用请求、兜底卡指引右上角浏览
器打开;非微信 data-src 懒挂载+9s 弱兜底+noscript 兜底。正文
入口卡双轨已渲染。

车道:op-gitea(专项四断言+E2E 34/34);收编复验六断言全过
(iframe/授权/微信降级/en/入口卡/零 PDF)

验证: E2E 34/34;微信 UA 模拟 is-wechat=true src 空 提示在;
      git status 零 .pdf

Spec: none (论文阅读页,车道级交付)

Co-authored-by: GLM-5.3
2026-11-02 08:41:39 +08:00
wait4xandGLM-5.3 bbb52b9e24 docs(post): 删除《单人博客的多智能体工程》(用户拍板)
与三篇工具详解定位重叠,用户选择保留详解三部曲。删除为单点
操作:无 en 版、无 translation-state 条目、无他文内链引用;构
建 42 页、lint 43 页 0 违例复验通过。

验证: pnpm astro build 42 页;content-lint 0 断链 0 缺图 0 配对
      警告;dist 无残留页

Spec: none (内容删除,用户决策)

Co-authored-by: GLM-5.3
2026-11-02 08:39:02 +08:00
wait4xandGLM-5.3 8571acdc8b feat(post): 工具三部曲——spec-harness/pi-mesh 全家桶/Agensh 论文笔记
用户直派车道交付:①Spec Harness 详解(任务包解剖+流水线图,
boundary/verify 实例+12 命令+门禁)②pi-mesh 四件套(stack 图+
合奏循环图+上手安装)③Agensh 论文笔记(三支柱/协作环/规模收
益图,DeLM 血统+issue#13→pi-board 诞生线)。七张自绘 SVG、双
语(en 手写直译 locked:true,禁 TMT 覆盖)。与既有 spec-
workflow-and-mesh(实战复盘向)互补并存。

车道:worker-one(build/lint/外链 curl/视觉两轮 7/7 全绿);收
编复验 E2E 34/34+lint 43 页 0/0/0

验证: pnpm astro build 39 文;E2E 34/34;content-lint 0/0/0

Spec: none (内容三篇,直派车道收编)

Co-authored-by: GLM-5.3
2026-11-01 13:35:27 +08:00
wait4xandGLM-5.3 127f349fb2 feat(gallery): 灯箱 v2.1 十项全量——起飞开合/隐层/侧置 meta/HD
①FLIP 原位起飞/回落(克隆双态矩形插值,关闭回落取缩略图实时
矩形)②背景点击关(containRect 几何判定——img 盒铺满 figure 的
元素链判定失效坑)③chrome 双层淡入+i 键/zoom 自动隐(换片重置)
④相邻预载 ⑤双指缩放 ⑥换片侧向过渡 ⑦缩放指示+首开提示 ⑧竖
图侧置 meta ⑨>1.5x HD 原图交换+404 兜底 ⑩竖拖关闭。reduced-
motion 全豁免;钳制数学保留。

验收三连修:chrome-in 动画 both 填充锁死 opacity(animation:none
掐填充——动画填充压制普通声明的级联陷阱);× 钮独立于双层
chrome 补隐(全隐口径=只剩图,Esc/i 逃逸);视觉终审四问修复
后全过(起飞连贯/侧置成立/隐层纯净/亮暗同判)。

车道:op-artalk(专项逐项+E2E 34/34);收编验收+视觉终审双绿

验证: E2E 34/34;三态隐层 computed 实测(i 隐/复原/zoom 隐全
过);全隐残留零(×/caption/箭头);Esc 逃逸 ✓

Spec: none (灯箱 v2.1 定版,车道级交付)

Co-authored-by: GLM-5.3
2026-11-01 13:21:15 +08:00
wait4xandGLM-5.3 d8997c3c72 feat(gallery): 灯箱 v2 收编——caption 字段全链+21 条视觉轨拟稿
schema caption?:string(人为描述,注释区别 alt 无障碍语义)+
Sveltia textarea 字段+21 条 json 全量上稿(视觉轨看图代拟策展
语气,用户可后台逐条改);GalleryPage 灯箱重构与缩放交互已随
44626f8/54769bc 入库,本笔补数据面。

车道:op-artalk(专项 8/8+E2E 34/34);视觉复核三问 PASS
(舞台纯净/meta 分层/caption 策展语气),唯一需修点=放大态
caption 遮挡→v2.1 ③ zoom 自动隐层已立项在治

验证: E2E 34/34;caption 渲染实测「落日把海与城一齐烫成金
箔…」;content-lint 0/0/0;schema 过构建

Spec: none (灯箱 v2 数据面收编,交互面已在库)

Co-authored-by: GLM-5.3
2026-11-01 12:51:20 +08:00
wait4xandGLM-5.3 44626f8edc fix(lightbox): 回 1x 自动归中+平移四向钳制(用户实测 bug)
用户实测:缩放回 100% 图片不回中显示不全;另放大态可拖飞出舞台。
三修:zoomAt next<=1 归零 translate;applyZoom 平移钳制 |t|≤尺
寸差/2(放大态不变量=图盒覆盖舞台而非缩在视口内);滚轮下限
0.5x→1x(fit 即最小,0.5x 制造偏移态)。mockup 同步三修。

验证: 回 1x transform=none 实测;四角极限拖拽覆盖舞台不变量
全过;E2E 34/34

Spec: none (用户实测 bug 热修,线上 v2+mockup 双修)

Co-authored-by: GLM-5.3
2026-11-01 12:34:11 +08:00
wait4xandGLM-5.3 54769bcfdf docs(research): 灯箱深度美化探索+可交互 mockup(v2.1 提案)
五站研究(PhotoSwipe 源码级核对/medium-zoom/Unsplash◐/图虫◐/Flickr)
→ 六维差距 → v2.1 增强提案十项(P0=原位起飞开合+背景点击关;P1=
chrome 双层淡入/相邻预载/双指缩放;P2=换片过渡/缩放指示/长图侧置
meta/HD 交换/竖拖关闭)+ 明确不做清单八条 + 可交互单文件 mockup
(仓内实拍 5 张、13 项交互可点)。

验证: mockup 引用路径 ../content/photos/ 相对仓根可开;文档引文对
PhotoSwipe defaultOptions 源码核对注记

Spec: none (研究文档+设计预览稿,v2.1 立项待用户拍板)

Co-authored-by: GLM-5.3
2026-11-01 12:09:51 +08:00
wait4xandGLM-5.3 541710b50a feat(post): 单人博客的多智能体工程——spec 与 mesh 实战复盘
四节+结语:19 连发压力日引入 / spec 任务包(blog-5f 实引 verify
原文与已证伪路径)/ mesh 交通规则(契约式下达-预约拦刀实录-守卫
铁律)/ 纪律从事故长出来(验收先探实值-口径错配-五段式运维-视觉
双轨-解封后 SEO 100)。全部数字与命令对档核验。

车道:worker-one(1906 字/schema 过/lint 0 违例);收编补引文精度
(T1 verify 对齐原文四条件)

验证: pnpm astro build 36 页;E2E 34/34;content-lint 0/0/0

Spec: none (内容文章,车道级交付)

Co-authored-by: GLM-5.3
2026-11-01 11:58:42 +08:00
wait4xandGLM-5.3 2b8353837a feat(ops): SEO 解封收尾脚本——default 混合文件逐块精确锚定
default 内四 server 块共存(三面板+博客),文件级 v3 脚本无法区
分;本脚本按各自 server_name 锚定:面板块补 noindex、博客块加
/admin/ 局部;幂等可重跑、备份出加载路径、nginx -t 失败回滚、
nginx -T 配置级终验(不受边缘缓存干扰)。

验证: bash -n 语法过;锚定正则对 default 实文件 server_name 四行
干跑(面板命中/博客精确/互不误中)

Spec: none (用户操作单自动化收尾件)

Co-authored-by: GLM-5.3
2026-11-01 11:17:45 +08:00
wait4xandGLM-5.3 3b65cc47ec fix(ops): seo-unblock v3 补——博客主站判定字符类修正
v2 残留:([[:space:]];|$) 被解析为空格+分号序列而非字符类,-x 兜底
吃行首缩进——admin location 两轮未落。统一 (^|[[:space:];]) 词边界
并显式排除 git 子域。

验证: bash -n 过;判定正则对 /etc/nginx/sites-enabled/default 实文件
干跑(博客行命中/git 行排除双验)

Spec: none (脚本热修 v3 补)

Co-authored-by: GLM-5.3
2026-11-01 11:16:21 +08:00
wait4xandGLM-5.3 ca7d71325c fix(ops): seo-unblock v3——补回判定反转名单制为 include 圈
v2 名单制漏 dpanel/ms365/ddns(同 default 文件 include snippet,剥
离后集体失守 noindex,实弹发现);v3 改凡 include esa-origin-ssl
且非博客主站一律补回,博客判定同步修正字符类。

验证: bash -n 语法过;判定正则对 default 实文件 server_name 复核

Spec: none (脚本热修 v3)

Co-authored-by: GLM-5.3
2026-11-01 11:16:03 +08:00
wait4xandGLM-5.3 d41fcc058c fix(ops): seo-unblock v2——备份出加载路径+主机判定精确 token
v1 实弹暴露两缺陷:①.bak-seo 备份留在 sites-enabled/ 内被 nginx
glob 加载→duplicate default server 全链崩;②grep -w 词边界点号
之误,git.wait4x.online 也被当博客主站注入 admin location。
v2:备份入 /tmp 专属目录+回滚后清理(含 v1 遗留)+server_name
精确 token 判定。

验证: bash -n 语法过;dry-run 计划段与判定正则复核

Spec: none (脚本热修:v1 事故根因双修)

Co-authored-by: GLM-5.3
2026-11-01 11:11:27 +08:00
wait4xandGLM-5.3 50c7595838 feat(ops): SEO 解封一键脚本——预览确认执行自检回滚五段式
nginx 侧全自动(snippet 剥离/内部 vhost 补回/admin 局部/失败回
滚/源站终验),ESA 三动作打印手动步骤;dry-run 先行+确认门。

验证: bash -n 语法过;dry-run 输出与 Phase 5f T1 操作单变更计划一致

Spec: none (用户操作单自动化,Phase 5f T1 收尾件)

Co-authored-by: GLM-5.3
2026-11-01 11:06:58 +08:00
wait4xandGLM-5.3 bb29c8abaf fix(deploy): 构建抓取通道可配+构建日志落盘——ESA 522 事故双根治
- build.sh:BLOG_GIT_URL 环境变量(默认公网,同机构建可指内网
  http://127.0.0.1:13020);每次构建 remote set-url 后 fetch——
  2026-10-26 事故:ESA 边缘对 git.wait4x.online 522 时公网 fetch
  set -e 静默死,staging 冻结 150s+ 无痕
- webhook_receiver.py:构建输出落盘 ~/docker_apps/blog/build.log
  (capture_output 丢弃=黑洞,本次事故"构建为何没跑"零证据);
  与运行副本双同步纪律维持
- 宿主 unit 补 BLOG_GIT_URL 内网通道(配置在 unit 不在仓——主机
  特定值不污染仓库默认)

验证: 手动构建实测 stamp=19b2390+robots.txt 352B 上线;18743 测试
      床恢复 200;smoke 对 staging 软门容忍符合 T1 转硬门前设计

Spec: none (发布链通道与可观测性修复,事故驱动)

Co-authored-by: GLM-5.3
2026-11-01 09:58:47 +08:00
wait4x 19b2390cbf Merge branch 'spec/2026-10-26_blog-5f'
Phase 5f 交付:SEO 解封仓库侧(robots+操作单)+ localePath 三处一刀治与 canonical 全量 + 构建期内容 lint/生产冒烟 + reader 纯文本模式 + 版式基底(流距 token/灰阶降档/字重/focus ring)+ 首屏 tagline + 图库暗衬底 + 代码块窗条 + 书评纸卡(视觉双轨复核 PASS)。

Spec: 2026-10-26_blog-5f
2026-11-01 09:51:27 +08:00
wait4xandGLM-5.3 983527a79d chore(spec): 归档 blog-5f——八任务收敛
Spec: 2026-10-26_blog-5f

Co-authored-by: GLM-5.3
2026-11-01 09:51:26 +08:00
wait4xandGLM-5.3 23ea8e5a55 chore(spec): 5.1 归位——全项目检查含升级条件+升级触发双字段
Spec: 2026-10-26_blog-5f

Co-authored-by: GLM-5.3
2026-11-01 09:49:52 +08:00
wait4xandGLM-5.3 57f19d855b chore(spec): 5.1 升级条件措辞对齐门禁
Spec: 2026-10-26_blog-5f

Co-authored-by: GLM-5.3
2026-11-01 09:42:01 +08:00
wait4xandGLM-5.3 a2395be379 chore(spec): T6 verify 管道笔误修正(-q 无输出喂 grep -v 恒退 1)
Spec: 2026-10-26_blog-5f

Co-authored-by: GLM-5.3
2026-11-01 09:40:04 +08:00
wait4xandGLM-5.3 90b8d0fbc0 test(e2e): ? 帮助浮层断言加固——焦点残留竞态三连治
面板测试后焦点可能残留输入域致 ? 键被吞(今日第三次偶发):
Esc 收场+activeElement blur+重试三连,3/3 稳定复现。

验证: E2E_BASE=dist 口连跑三轮 34/34 全绿

Spec: 2026-10-26_blog-5f

Co-authored-by: GLM-5.3
2026-11-01 09:34:47 +08:00
wait4xandGLM-5.3 4e24fe510f chore(spec): verify 命令统一 E2E_BASE 本地 dist 口
staging=origin/main 纪律下 spec 分支在途代码的 E2E 只能打本地
dist——T2 canonical 断言对 staging 必挂属预期非缺陷。

Spec: 2026-10-26_blog-5f

Co-authored-by: GLM-5.3
2026-11-01 09:31:48 +08:00
wait4xandGLM-5.3 2bfe2ef3db feat(5f): 代码块窗条+书评纸卡(T5/T6 车道收编,视觉复核 PASS)
- T5:≥4 行块挂 .code-win 窗条(JS 数行标类+伪元素渲染:语言
  标签取 shiki pre[data-language]+灰阶三点 #30363d 投影法一点化
  三点,弃红黄绿;条 #161b22 恒深区亮暗同判);单行/2行裸块;
  csge 文 4/18 判定边界实测准确;scoped 够不着 Content 内 pre
  坑用 :global 解
- T6:书评正文圆角纸卡(亮 #f4f4f2/暗 #201d18 暖棕夜灯,零新
  变量吃 T7 已入 tokens;1px hairline 四边抑制长文竖线感;标题
  区外置结构未动)
- 视觉复核(glm-5.3-flash:max 像素级):T5 四项全 PASS(三点实
  采样无红黄绿、条/点/体 github-dark 同族灰、与块顶圆角对齐);
  T6 双态 PASS;暗态窗条未入截图框为残留观察项(恒定 CSS 推定
  成立,判词 /tmp/beauty-impl/verdict.md)
- tasks.md T5 verify 落点修正(src/pages/archives,车道如实报
  告);E2E ? 浮层偶发竞态 2/2 复跑绿

车道:op-artalk(自验 5/5+2/2);收编验收:窗条 4/18+data-lang+
纸卡双态 computed+视觉双卡 PASS

验证: E2E 34/34 PASS(E2E_BASE=dist 口);窗条/纸卡 computed 与
      像素采样全对

Spec: 2026-10-26_blog-5f

Co-authored-by: GLM-5.3
2026-11-01 09:31:22 +08:00
wait4xandGLM-5.3 814b138f89 test(e2e): E2E_BASE 环境变量覆写——本地 dist 口验未推代码
staging=origin/main 纪律下,spec 分支在途代码的 E2E 只能打本地
dist;此前 BASE 硬编码迫使车道手工绕(T2 收编验收实锤)。

验证: E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs 34/34
      PASS(含新 canonical 断言)

Spec: 2026-10-26_blog-5f

Co-authored-by: GLM-5.3
2026-11-01 09:13:44 +08:00
wait4xandGLM-5.3 c307285a25 fix(i18n): localePath 三处双 en 一刀治+canonical 全量(blog-5f T2/T3 车道收编)
- T2:localePath 先归一 zh 再拼前缀(Header/CommandPalette/
  BaseLayout 三处 /en/en/ 同源病根一刀),返回值统一尾斜杠;
  BaseLayout 自指 canonical 全量 35 页;E2E 新增 en canonical
  自指断言(33→34)
- T3:content-lint.mjs 零依赖三检(断链 FAIL/缺图 FAIL/zh-en
  配对 WARN;CONTENT_LINT 逃生门;photos-originals 叠加层与
  /admin 单语两误报已修);prod-smoke.sh 三断言(robots 未落明
  确 FAIL 不静默);build.sh 守卫行挂载(smoke 对 staging 首版
  容忍,T1 robots 上线后转硬门,触发条件注释在案)
- tasks.md verify 字面量 33→34 联动同步(6 处)

车道:op-gitea(verify 六条实录);收编验收:E2E 34/34+产物零
/en/en/+lint 0/0/0 实跑复现

验证: node tests/e2e.mjs 34/34 PASS;! grep -r /en/en/ dist/;
      content-lint 三检 0 违例;bash -n+test -x 过

Spec: 2026-10-26_blog-5f

Co-authored-by: GLM-5.3
2026-11-01 09:12:01 +08:00
wait4xandGLM-5.3 8ec1712135 feat(5f): reader 模式+版式基底+首屏 tagline+图库质感(T4/T7/T8)
- T4 reader:html.reader 单 class(去装饰非去内容:隐辉光/图标
  钮/pv 行,ICP 合规底线永留;正文字号+1px 行宽 56ch;入场动效
  关);命令面板 reader 命令+r 键+localStorage;ThemeScript 预绘
  防闪
- T7 基底:--flow-* 五流距变量双轨应用;暗色 hairline 降档;
  页面标题 700→600(裁定 #6 减字重不减号);focus-visible ring
  统一;列表行距 +2px;--paper/--radius-img/--ease-out 入库
- T8:首页 IdCard slot 落 tagline「写作者 · 读者 · 快门按压者,
  拥抱开源。」;图库暗色 4% 轻衬底+圆角统一(裁定 #4 双模式分判)

验证: E2E 33/33;reader r 键触发+辉光 none+刷新保持实测;流距
      computed 0.6em 实算;tagline 渲染实测

Spec: 2026-10-26_blog-5f

Co-authored-by: GLM-5.3
2026-11-01 09:09:27 +08:00
wait4xandGLM-5.3 22930cfffd feat(seo): robots.txt 与 SEO 解封操作单(blog-5f T1)
robots 404 补齐(Allow 全站/Disallow admin+draft/Sitemap 行);
操作单三步走:nginx snippet 剥 noindex+内部 vhost 补回+admin 局部
保留、ESA purge+TTL+绕缓存、curl 三验证——用户侧执行入口。

验证: T1 verify 命令全过(robots 三断言+操作单节存在)

Spec: 2026-10-26_blog-5f

Co-authored-by: GLM-5.3
2026-11-01 09:06:54 +08:00
wait4xandGLM-5.3 5d18591967 chore(spec): blog-5f 任务清单格式修正
Spec: 2026-10-26_blog-5f

Co-authored-by: GLM-5.3
2026-11-01 09:06:02 +08:00
wait4xandGLM-5.3 c1e3d30954 chore(spec): 立项 blog-5f——SEO 解封+功能优化批+美化首批
Spec: 2026-10-26_blog-5f

Co-authored-by: GLM-5.3
2026-11-01 09:05:26 +08:00
wait4xandGLM-5.3 975cf5e58b docs(research): 美化方案并入视觉裁定(§6 七问定案)
两案之争全部落定:首屏=融合(加 tagline 不 banner 化);纸面板=
选圆角纸卡(书评轨);代码块=多行窗条+灰阶三点;图库=亮直贴/
暗轻衬底;热力图=亮灰维持暗色补救(绿系亮色不换);标题 27px
维持(要轻减字重不减号);灰阶 5 档够用(hairline 更淡+行距微
增)。视觉轨 glm-5.3-flash:max,25 张证据图亮暗双态逐问裁定。

验证: 裁定含信心等级+落地注意+双模式分判;与 §5 问题清单一一
      对应

Spec: none (方案定案文档,待用户审批实施)

Co-authored-by: GLM-5.3
2026-10-26 23:07:56 +08:00
wait4xandGLM-5.3 7c5455278b docs(research): 美化专项方案(文本轨,K-S 卡片,视觉待验标注)
现状审计 16 项(间距阶/灰阶档/圆角混用/流距/focus ring/代码块/
纸面板/热力图灰绿之争)× 5 站补充探索(rauno 12 级灰+paco alpha
阶梯+mattdesl 等宽层级+tw93 手写体温+antfu blur-up)× 卡片 K-S
(token 值级改法+computed-style 断言)× 截图问题清单 7 问(交
glm-5.3-flash:max 视觉轨复核中,verdict 合并后定案)。

验证: 截图床 /tmp/beauty-shots 25 张就位;文档 149 行入仓

Spec: none (方案文档文本轨,视觉裁定合并后呈审)

Co-authored-by: GLM-5.3
2026-10-26 22:55:05 +08:00
wait4xandGLM-5.3 6b35356e91 docs(research): 博客优化方案 v2(深度版,scout max 推理车道)
六站补面(rauno/diygod/xeiaso/shud.in/vincentwill/sabrina)+
自查实证(生产 SEO 60-63 根因=noindex 封印+robots 404+ESA 30 天
头缓存;localePath 双 en;Artalk 评论数 API 实测;reader 落点
清单)+ 实施卡片 A-J(文件级落点+断言验收+工作量)+ 三批排期 +
明确不做清单。

验证: 关键断言独立复核(nginx snippet noindex 行/robots 404 生产
      curl 实测一致)

Spec: none (方案文档,待用户批 A/B/C/E 立即批后立项)

Co-authored-by: GLM-5.3
2026-10-26 20:29:05 +08:00
wait4xandGLM-5.3 a9da765cb1 docs(research): 六站博客体验研究与优化方案(scout 车道)
qh1009/nbfox/yongyuan/阮一峰/antfu/innei 六站直连抓取分析:
共性 12 项(阅读时长/双语互链/纯文本模式/评论数前置/周刊化/
EXIF 元数据卡/归档全量/偏好面板等)+ 差异化护城河 6 项 +
Sveltia vs 主流后台差距与补齐方案 + P0-P2 分级建议表。

验证: 文档 125 行入仓可读;来源快照 /tmp/uxstudy/(会话级)

Spec: none (调研文档,方案待用户审阅后立项)

Co-authored-by: GLM-5.3
2026-10-26 20:05:00 +08:00
wait4xandGLM-5.3 3941a34f04 feat(pwa): SW 预缓存自动生成 + Sveltia 瞬间管理(池 4/5)
- gen-sw-precache.mjs post-build:按 dist 产物推导路由重写 dist/sw.js
  预缓存段(AUTO 标记锚定;规则=顶层+/en/ 轨深度≤1、/admin 永不入
  列)——新增顶层页自动进离线包,根治手工清单漏页维护债;实测 19
  路由含 moments 双轨、admin 排除
- build.sh 守卫挂钩(守卫先行纪律);sw.js 源内兜底清单保留
- Sveltia 加 moments 集合:随手发瞬间(text/image 可选+hint 自查条
  件必填、pubDate 日期件、tags、slug 日期模板)——管理后台从只管
  照片扩到内容面第一步

验证: 产物 sw 19 路由断言全过;E2E 33/33 PASS;yaml 解析过;bash -n 过

Spec: none (离线包自动化+后台扩容,车道级交付)

Co-authored-by: GLM-5.3
2026-10-26 20:02:32 +08:00
wait4xandGLM-5.3 3a59edc79a feat(nav): 书架进导航(车道 G)——图标钮出舱钉死右侧
- 评估实数:第 6 项增量 zh +34-44/en +49-51px,旧结构 en 达
  102-125px 滚区且主题钮出首屏——收纳策略=搜索/语言/主题三钮
  移出 nav 滚动区钉死 .nav-actions 常驻右侧,内滚只波及文本项,
  任何宽度三功能钮可见(搜索是移动端主发现路径不可藏)
- nav 序:开源 · 文章 · 书架 · 图库 · 瞬间 · 关于(书架=阅读轨
  延伸紧跟文章);ui.ts +nav_books/kbd_gs_books
- Keyboard g-chord 增 s 键(g s→书架,双轨前缀路由),帮助表
  按既有 kbd_* 词条模式接入
- E2E en 导航断言扩为 ≥6 且含 /en/books/
- 已知余量:en 390-430 文本区滚 ~114px(词条已最短化,再省需砍
  语义留观);zh 全宽段 1-16px 基本无感

车道:op-gitea(八格扫描+E2E 33/33);收编验收:序/双轨 href/
三宽度三功能钮可见性/g s 实跳全过

验证: pnpm astro build exit 0;本地 dist 六项 playwright 全过;
      E2E 33/33(推送后 staging 复验)

Spec: none (导航扩容+出舱策略,车道级交付)

Co-authored-by: GLM-5.3
2026-10-25 22:47:17 +08:00
wait4xandGLM-5.3 73c31ffb2a feat(pwa): 瞬间页安装引导与启动快捷方式(车道 H)
- manifest shortcuts:瞬间直达(/moments/ + 复用 192 图标;lang
  zh 单份中文)
- 安装 CTA:时间线后灰阶虚线小条+ghost 按钮;beforeinstallprompt
  模块级立即绑定(跨软导航存活),三态=可装显示/已装
  (display-mode:standalone 或 iOS standalone)永不显/不支持默认
  hidden;prompt 后 accepted/appinstalled 清引用;文案组件内
  locale 双份
- SW 复核零改动:PRECACHE 已含双轨瞬间、IMMUTABLE 覆盖
  /_astro/ 瞬间图片管线、NEVER_CACHE 拦 pv 走 '–' 降级——三处
  现成正确;离线实证:断网瞬间页 200+时间线完整+pv 降级;图片
  0/0 为数据现状(3 条瞬间均无图),同管线文章页图片离线 1/1
  等价实证成立

车道:op-artalk(自验 11/11);收编验收:CTA hidden 态+离线 200+
标题+manifest shortcuts 解析实测(首轮探针 context API 误用致假
FAIL,selector 复核全过)

验证: pnpm astro build exit 0;E2E 33/33;断网瞬间页 playwright
      实测 200

Spec: none (PWA 增强:shortcuts+CTA+离线实证,车道级交付)

Co-authored-by: GLM-5.3
2026-10-25 22:45:18 +08:00
wait4xandGLM-5.3 215760c38a style(nav): en 开源词条 Open Source→Projects——窄屏免内滚
390-430px 尾部主题钮原本需导航内滚一滑才可见;短词省 35px,
三宽度全直显(用户拍板)。页面标题与热力图词条不动,仅导航。

验证: en@390/410/430 主题钮右缘全在屏内+页面零溢出实测;E2E
      33/33 PASS

Spec: none (单词条更名,宽度收益实测)

Co-authored-by: GLM-5.3
2026-10-25 22:32:02 +08:00
wait4xandGLM-5.3 55cfc73cb7 feat(interact): 访问量小字+瞬间时间线动效(车道 F)
- pv:ArtalkComments/MomentsPage 组件级独立 POST /artalk/api/v2/
  pages/pv(页面加载即计,不等懒加载;POST 天然绕 ESA 边缘缓存);
  init 加 pvAdd:false 防双计(默认 true 此前评论加载一直在默默计
  数——mian-sha pv=2 即此来源);双轨 pageKey 分开计数
- 瞬间动效:.tl-item IntersectionObserver 进入视口一次性渐显
  (opacity 0+8px);降级链=data-mom-anim 前置标记——无 JS/
  reduced-motion/无 IO 三情形标记缺席则初始隐藏不生效;软导航
  astro:page-load 重注册
- Artalk 后台真根因修复(车道侧宿主件,非仓内):容器日志实锤
  登录从未成功过(12+ 次 401/400,非会话丢失)——CLI 重置密码+
  凭据入 INITIAL-CREDENTIALS.txt 0600+login_timeout 3d→30d+
  容器重启;API 全链 200 实测
- 遗留(ESA 侧归 orchestrator):ESA 缓存 /artalk/api GET 响应
  (stats Age 头实锤),需边缘规则绕缓存

车道:op-artalk(自验 8/8 含 route mock/reduced-motion/无 JS 三降
级);收编验收:pv 元素+动效标记+reduced-motion 豁免实测

验证: pnpm astro build exit 0;E2E 33/33;三降级链 playwright 实测

Spec: none (pv+动效,车道级交付)

Co-authored-by: GLM-5.3
2026-10-25 22:19:41 +08:00
wait4xandGLM-5.3 d00ca7e447 fix(i18n): 语言轨导航修复+重排,热力图切 GitHub(车道 E)
- 真 bug(用户实测:切 en 后点板块回中文):Header nav href 硬编码
  zh 路径,curLocale 只喂了词条——localeOf(Astro.url) 构建期自推断
  +五项+品牌链接全轨化(brand 原指 / 同病同修),aria 同步
- 导航重排(用户拍板):开源 · 文章 · 图库 · 瞬间 · 关于;瞬间
  新进导航;移动 320-430×双轨八格页面级溢出全 0(内滚阈值 350→450
  +中段地板再收;en 390-430 尾部约 55px 靠内滚——'Open Source'→
  'Projects' 改名可省 35px,留用户拍板)
- 热力图纯切 GitHub(用户拍板):GraphQL contributionsCollection
  53 周窗口(上海日界);token=宿主 0600 github-token.txt,缺位→
  WARN 降级样例数据(当前站点即样例);三级回落 token→样例→空网格
- E2E 新增 2 断言:en 页 nav 全 /en/ 前缀+brand 指 /en/(本 bug
  的回归测试)

车道:op-gitea(自验 33/33);收编验收:en/zh 序实测+三宽度溢出 0+
E2E 33/33(本地 dist 口径——staging 未含未推代码为既定纪律)

验证: pnpm astro build 34 页 exit 0;node tests/e2e.mjs 33/33;
      en/zh 导航序与 href 前缀 playwright 实测

Spec: none (Header 修复+重排+热力图换源,车道级交付)

Co-authored-by: GLM-5.3
2026-10-25 22:19:40 +08:00
wait4xandGLM-5.3 363cc616f7 feat(motion): 动效层三件——背景辉光/入场/心跳,页脚撤双入口
- 背景鼠标辉光:ink 色 4% 径向渐变跟随(pointer:fine×非
  reduced-motion 才有 JS,raf 节流 document 级监听 VT 存活)——
  丙灰阶语言的"活背景",触屏/减动效自动隐藏
- 正文入场 rise-in 12px/0.4s(VT 换页自然重放)+ 列表行悬停
  1px 微浮;全量 prefers-reduced-motion 豁免
- 首页"最近心跳":构建期拉 blog 仓最新 commit(gitea API 8s 超时
  静默降级),脉冲点+相对时长——本站自己的脉搏
- 页脚撤「装备/制作」入口(页面保留,用户决策:只删入口)
- E2E 加固:g-chords 修好后 location.href 与 goto 竞态(en 页弦
  导航迟到提交打断 goto)——中断重试模式
- 过程教训:str.replace 静默无操作再犯(锚点瞄错组件文件)——
  脚本未进产物却报"全落地",replace 必须 assert 配对 grep 产物

验证: --mx 鼠标双跳 15.6% 实测跟随;E2E 31/31 PASS(竞态重试后
      确定性);构建 34 页 exit 0

Spec: none (动效层+页脚撤口+测试加固,车道级交付)

Co-authored-by: GLM-5.3
2026-10-25 22:09:41 +08:00
wait4xandGLM-5.3 0b6524f53c perf(home): 首屏 eager+关键 CSS 内联,Lighthouse 伪影根因修毕
- 车道 D 三件收编(op-gitea):①index.astro 首两图 eager+首图
  fetchpriority=high(Image 默认 lazy 曾压 LCP 至 6.4s,好模式现
  1.3s)②inline-home-css.mjs:BaseLayout 11KB 内联首页 head(省
  发现+下载跳)③build.sh 守卫挂钩
- 诊断结论入档:home 中位漂移根因=测试床伪影(python server 不压
  缩,裸传 67KB vs 生产 gzip 9.2KB;lantern 对未压缩 HTML 流式归因
  双峰 99↔165ms 翻转出两个世界)——gzip 等价口径中位 100
  (FCP 0.9-1.1s/LCP 1.3-1.4s/TBT 0/CLS 0)
- 测试床转正:gz-server.py 泛化入仓(ThreadingHTTPServer+gzip
  [端口 目录] 参数化)——兼治 SW 预缓存风暴竞速(单线程 server
  曾把 E2E /gallery/ networkidle 饿死,实锤于收编验收)
- E2E serviceWorkers:'block':SW 有专属测试,站点 E2E 求确定性
- 三连否定留档:SW✗/css链✗/子集内联 v3✗(6×63 反例钉死,已
  回滚)——残尾 20% 离群属 lantern trace 抖动,采中位+多轮

车道:op-gitea(过程三连否定+测试床移交);收编验收:内联 10KB
实出+首图 eager 实证+E2E 31/31(SW 屏蔽后确定性复现)

验证: gz 测试床 18743 gzip 10KB(生产等价口径);node tests/e2e.mjs
      31/31 PASS;构建+守卫链复跑

Spec: none (性能三件+测试床转正,车道级交付)

Co-authored-by: GLM-5.3
2026-10-25 21:37:38 +08:00
wait4xandGLM-5.3 71b8985c17 fix(keyboard): g g chord 不可达——路由判断先于武装分支
Phase 1 潜伏缺陷:g 键分支先于路由检查 return,第二击 g 永远
重新武装,routes['g'](gallery)全站不可达。修序即愈,g→g 自身
是合法 chord。en 轨随上一提交前缀化后同步受益。

验证: chord 矩阵 5 检(zh/en × g g/g h/g m 实跳目标全中);
      E2E 31/31 PASS

Spec: none (keydown 分支修序,矩阵测试入档)

Co-authored-by: GLM-5.3
2026-10-25 21:18:58 +08:00
wait4xandGLM-5.3 3e73fe957e fix(i18n): g-chords 导航目标随轨前缀化——en 页 g h 跳 /en/
C 车道已知边界补刀:routes 表构建期按 locale 前缀化,en 页
g-chords 不再跳中文轨。

验证: playwright 双轨 g-chords 实跳(en g h→/en/、zh g g→
/gallery/);E2E 31/31 PASS

Spec: none (五路由前缀化一行模式,C 车道边界补刀)

Co-authored-by: GLM-5.3
2026-10-25 21:14:56 +08:00
1223 changed files with 16531 additions and 978 deletions

No files matched your search

@@ -0,0 +1,95 @@
# Phase 5f:SEO 解封与功能优化批 + 美化首批 - 完成总结
## 交付结论
- 结果:完成
- 完成时间:2026-11-01 09:51
## 假设回顾
### 已验证假设
- nginx sudo 与 ESA 控制台由用户操作(本包只备好仓库侧与操作单)
- 视觉裁定为终态依据;实现与裁定冲突时以裁定为准并在完成总结记录
### 仍未完全验证的假设
- 无
## 交付范围
### 已交付
- T1 SEO 仓库侧三件(robots.txt + admin noindex 预案 + 用户操作单)
- T2 localePath 双 en 修复 + 自指 canonical + 尾斜杠归一
- T3 构建期内容 lint + 生产冒烟 + build.sh 守卫挂载
- T4 reader 纯文本模式(html.reader + 命令面板 + r 键 + 持久化)
- T5 代码块窗条(P 卡:≥4 行灰阶窗+mono 标题+灰阶三点)
- T6 书评纸面板(Q 卡:圆角米灰纸卡+暗色暖棕+hairline 边框)
- T7 版式基底(K/L/O 卡:流距 token+灰阶补档+暗色 hairline 降档+微交互曲线+focus ring)
- T8 首屏 tagline + 图库质感(M/N 卡:身份语+圆角统一+暗色衬底)
### 未交付
- 无
### 偏差说明
- 无
## 简化决策
### 保持简单的关键选择
- 最简单可行方案:token 值级 CSS 改动为主 + locale.ts 单函数修 + 两个构建脚本;无新依赖
- 暂不引入:blur-up 占位(需管线)、独立设置面板
- 不做的抽象:主题引擎化/样式体系重构(tokens 增量演化)
### 本轮明确不做的内容
- nginx/ESA 实际操作(用户侧);draft 路由 D、评论数 F、EXIF 卡 G、title H(本月批后续包)
## 变更边界
### 本轮主要改动模块
- 见 tasks.md 的 boundary 记录
### 明确未触碰的区域
- nginx/ESA 实际操作(用户侧);draft 路由 D、评论数 F、EXIF 卡 G、title H(本月批后续包)
## 验证证据
### 备注
- 机器证据:runs.jsonl(见验证范围的机器验证率)
## 验证范围
- 级别:package
- 机器验证率:8/8
- 规则:check 与 done 复用该范围;只有记录升级触发时才执行全项目检查
## 哲学生效证据
### 行为成效回填
- Development Record 行为门禁通过
### 一致性门禁回顾
- 验收面(checklist 或成功标准)已通过
## Git 记录
- 日期时间:2026-11-01 09:51
- 范围:Phase 5f:SEO 解封与功能优化批 + 美化首批
- 功能:T1 SEO 仓库侧三件(robots.txt + admin noindex 预案 + 用户操作单);T2 localePath 双 en 修复 + 自指 canonical + 尾斜杠归一;T3 构建期内容 lint + 生产冒烟 + build.sh 守卫挂载;T4 reader 纯文本模式(html.reader + 命令面板 + r 键 + 持久化);T5 代码块窗条(P 卡:≥4 行灰阶窗+mono 标题+灰阶三点);T6 书评纸面板(Q 卡:圆角米灰纸卡+暗色暖棕+hairline 边框);T7 版式基底(K/L/O 卡:流距 token+灰阶补档+暗色 hairline 降档+微交互曲线+focus ring);T8 首屏 tagline + 图库质感(M/N 卡:身份语+圆角统一+暗色衬底)
- 操作:提交 / 推送
- 成效:Development Record 行为门禁通过
- 提交:pending local commit
- 提交区间:975cf5e58bf2846fc0c1e34a600725bcfe915f0e..23ea8e5a555a984cf0e8be46efecb718d26d3cdc
- 推送:not-run (push stage follows commit)
## AGENTS.md 增量提议
```diff
Phase 5f 教训与资产:①收官门禁代码化(spec_gate.py 首秀连环拦下五宗——E2E 口径错配/竞态雷/verify 管道笔误/5.1 双字段措辞/字段改名丢失,每次给出可执行下一步);②E2E_BASE 覆写为 spec 验收标准姿势;③视觉双轨闭环(token 方案→flash:max 像素验收);④? 浮层焦点竞态根治;⑤车道 verify 落点偏差随回执报告;⑥add -A 卷入在途车道第四次变体秒拆;⑦verify 断言禁用 -q 接管道;⑧spec 模板字段名不可创意改写(升级触发≠升级条件)。
```
## 被拒绝的扩展提议
- 无
## 门禁证据
- check_spec_package.py exit 0
## 遗留事项
- 无;问题处置见下方结构化区块
## 问题处置
```json
{
"issues": [],
"version": 1
}
```
@@ -0,0 +1,48 @@
{"task": "task_0001", "cmd": "test -f public/robots.txt && grep -q \"Sitemap: https://wait4x.online/sitemap-index.xml\" public/robots.txt && grep -q \"Disallow: /admin\" public/robots.txt && grep -q \"SEO 解封操作单\" docs/上线检查单.md", "exit": 0, "duration_s": 0.005, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "b8b3033256d8436e1994365e0a4d843de899a400abcd303c4a3f8703bf7236ec", "at": "2026-11-01T01:31:23+00:00"}
{"task": "task_0002", "cmd": "! grep -r \"/en/en/\" dist/ && grep -q 'rel=\"canonical\"' dist/en/archives/mian-sha/index.html && node tests/e2e.mjs 2>&1 | grep -q \"34/34 PASS\"", "exit": 1, "duration_s": 0.669, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "b8b3033256d8436e1994365e0a4d843de899a400abcd303c4a3f8703bf7236ec", "at": "2026-11-01T01:31:23+00:00"}
{"task": "task_0003", "cmd": "node scripts/build-perf/content-lint.mjs dist && bash -n infra/deploy/build.sh && test -x scripts/build-perf/prod-smoke.sh", "exit": 0, "duration_s": 0.06, "stdout_tail_sha256": "209fba8e31f47e3c0984b06e53b6dec06a74dcc59311dae22cca8323af4ad09f", "tree_fp": "b8b3033256d8436e1994365e0a4d843de899a400abcd303c4a3f8703bf7236ec", "at": "2026-11-01T01:31:23+00:00"}
{"task": "task_0004", "cmd": "node tests/e2e.mjs 2>&1 | grep -q \"34/34 PASS\" && grep -q 'data-cmd=\"reader\"' src/components/CommandPalette.astro && grep -q \"html.reader\" src/styles/global.css", "exit": 1, "duration_s": 0.637, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "b8b3033256d8436e1994365e0a4d843de899a400abcd303c4a3f8703bf7236ec", "at": "2026-11-01T01:31:24+00:00"}
{"task": "task_0005", "cmd": "node tests/e2e.mjs 2>&1 | grep -q \"34/34 PASS\" && grep -rqE \"code-win|codebar|窗条\" src/pages/archives/", "exit": 1, "duration_s": 0.675, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "b8b3033256d8436e1994365e0a4d843de899a400abcd303c4a3f8703bf7236ec", "at": "2026-11-01T01:31:25+00:00"}
{"task": "task_0006", "cmd": "node tests/e2e.mjs 2>&1 | grep -q \"34/34 PASS\" && grep -rq \"paper\" src/components/ | grep -v node_modules", "exit": 1, "duration_s": 0.638, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "b8b3033256d8436e1994365e0a4d843de899a400abcd303c4a3f8703bf7236ec", "at": "2026-11-01T01:31:25+00:00"}
{"task": "task_0007", "cmd": "node tests/e2e.mjs 2>&1 | grep -q \"34/34 PASS\" && grep -q \"flow-h2-pre\" src/styles/tokens.css && grep -q \"focus-visible\" src/styles/global.css", "exit": 1, "duration_s": 0.656, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "b8b3033256d8436e1994365e0a4d843de899a400abcd303c4a3f8703bf7236ec", "at": "2026-11-01T01:31:26+00:00"}
{"task": "task_0008", "cmd": "node tests/e2e.mjs 2>&1 | grep -q \"34/34 PASS\" && grep -rq \"tagline\" src/pages/index.astro src/components/ && grep -q \"radius-img\" src/styles/tokens.css", "exit": 1, "duration_s": 0.66, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "b8b3033256d8436e1994365e0a4d843de899a400abcd303c4a3f8703bf7236ec", "at": "2026-11-01T01:31:27+00:00"}
{"task": "task_0001", "cmd": "test -f public/robots.txt && grep -q \"Sitemap: https://wait4x.online/sitemap-index.xml\" public/robots.txt && grep -q \"Disallow: /admin\" public/robots.txt && grep -q \"SEO 解封操作单\" docs/上线检查单.md", "exit": 0, "duration_s": 0.005, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "7c6557e6c8bfbe5baff62bc1881f03fd8d53750b60ad5fd843888b0db2932995", "at": "2026-11-01T01:31:48+00:00"}
{"task": "task_0002", "cmd": "! grep -r \"/en/en/\" dist/ && grep -q 'rel=\"canonical\"' dist/en/archives/mian-sha/index.html && E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs 2>&1 | grep -q \"34/34 PASS\"", "exit": 0, "duration_s": 15.103, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "7c6557e6c8bfbe5baff62bc1881f03fd8d53750b60ad5fd843888b0db2932995", "at": "2026-11-01T01:32:03+00:00"}
{"task": "task_0003", "cmd": "node scripts/build-perf/content-lint.mjs dist && bash -n infra/deploy/build.sh && test -x scripts/build-perf/prod-smoke.sh", "exit": 0, "duration_s": 0.073, "stdout_tail_sha256": "209fba8e31f47e3c0984b06e53b6dec06a74dcc59311dae22cca8323af4ad09f", "tree_fp": "7c6557e6c8bfbe5baff62bc1881f03fd8d53750b60ad5fd843888b0db2932995", "at": "2026-11-01T01:32:04+00:00"}
{"task": "task_0004", "cmd": "E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs 2>&1 | grep -q \"34/34 PASS\" && grep -q 'data-cmd=\"reader\"' src/components/CommandPalette.astro && grep -q \"html.reader\" src/styles/global.css", "exit": 1, "duration_s": 14.313, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "7c6557e6c8bfbe5baff62bc1881f03fd8d53750b60ad5fd843888b0db2932995", "at": "2026-11-01T01:32:18+00:00"}
{"task": "task_0005", "cmd": "E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs 2>&1 | grep -q \"34/34 PASS\" && grep -rqE \"code-win|codebar|窗条\" src/pages/archives/", "exit": 0, "duration_s": 14.413, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "7c6557e6c8bfbe5baff62bc1881f03fd8d53750b60ad5fd843888b0db2932995", "at": "2026-11-01T01:32:32+00:00"}
{"task": "task_0006", "cmd": "E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs 2>&1 | grep -q \"34/34 PASS\" && grep -rq \"paper\" src/components/ | grep -v node_modules", "exit": 1, "duration_s": 14.48, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "7c6557e6c8bfbe5baff62bc1881f03fd8d53750b60ad5fd843888b0db2932995", "at": "2026-11-01T01:32:47+00:00"}
{"task": "task_0007", "cmd": "E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs 2>&1 | grep -q \"34/34 PASS\" && grep -q \"flow-h2-pre\" src/styles/tokens.css && grep -q \"focus-visible\" src/styles/global.css", "exit": 1, "duration_s": 14.192, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "7c6557e6c8bfbe5baff62bc1881f03fd8d53750b60ad5fd843888b0db2932995", "at": "2026-11-01T01:33:01+00:00"}
{"task": "task_0008", "cmd": "E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs 2>&1 | grep -q \"34/34 PASS\" && grep -rq \"tagline\" src/pages/index.astro src/components/ && grep -q \"radius-img\" src/styles/tokens.css", "exit": 0, "duration_s": 14.446, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "7c6557e6c8bfbe5baff62bc1881f03fd8d53750b60ad5fd843888b0db2932995", "at": "2026-11-01T01:33:15+00:00"}
{"task": "task_0001", "cmd": "test -f public/robots.txt && grep -q \"Sitemap: https://wait4x.online/sitemap-index.xml\" public/robots.txt && grep -q \"Disallow: /admin\" public/robots.txt && grep -q \"SEO 解封操作单\" docs/上线检查单.md", "exit": 0, "duration_s": 0.005, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "a7774fa671522fb695d781169a132c4dfe108f5de2a8fd911e8e2993f16f10e6", "at": "2026-11-01T01:34:48+00:00"}
{"task": "task_0002", "cmd": "! grep -r \"/en/en/\" dist/ && grep -q 'rel=\"canonical\"' dist/en/archives/mian-sha/index.html && E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs 2>&1 | grep -q \"34/34 PASS\"", "exit": 0, "duration_s": 14.521, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "a7774fa671522fb695d781169a132c4dfe108f5de2a8fd911e8e2993f16f10e6", "at": "2026-11-01T01:35:02+00:00"}
{"task": "task_0003", "cmd": "node scripts/build-perf/content-lint.mjs dist && bash -n infra/deploy/build.sh && test -x scripts/build-perf/prod-smoke.sh", "exit": 0, "duration_s": 0.056, "stdout_tail_sha256": "209fba8e31f47e3c0984b06e53b6dec06a74dcc59311dae22cca8323af4ad09f", "tree_fp": "a7774fa671522fb695d781169a132c4dfe108f5de2a8fd911e8e2993f16f10e6", "at": "2026-11-01T01:35:02+00:00"}
{"task": "task_0004", "cmd": "E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs 2>&1 | grep -q \"34/34 PASS\" && grep -q 'data-cmd=\"reader\"' src/components/CommandPalette.astro && grep -q \"html.reader\" src/styles/global.css", "exit": 0, "duration_s": 14.398, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "a7774fa671522fb695d781169a132c4dfe108f5de2a8fd911e8e2993f16f10e6", "at": "2026-11-01T01:35:17+00:00"}
{"task": "task_0005", "cmd": "E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs 2>&1 | grep -q \"34/34 PASS\" && grep -rqE \"code-win|codebar|窗条\" src/pages/archives/", "exit": 0, "duration_s": 14.495, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "a7774fa671522fb695d781169a132c4dfe108f5de2a8fd911e8e2993f16f10e6", "at": "2026-11-01T01:35:31+00:00"}
{"task": "task_0006", "cmd": "E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs 2>&1 | grep -q \"34/34 PASS\" && grep -rq \"paper\" src/components/ | grep -v node_modules", "exit": 1, "duration_s": 14.653, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "a7774fa671522fb695d781169a132c4dfe108f5de2a8fd911e8e2993f16f10e6", "at": "2026-11-01T01:35:46+00:00"}
{"task": "task_0007", "cmd": "E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs 2>&1 | grep -q \"34/34 PASS\" && grep -q \"flow-h2-pre\" src/styles/tokens.css && grep -q \"focus-visible\" src/styles/global.css", "exit": 0, "duration_s": 14.506, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "a7774fa671522fb695d781169a132c4dfe108f5de2a8fd911e8e2993f16f10e6", "at": "2026-11-01T01:36:00+00:00"}
{"task": "task_0008", "cmd": "E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs 2>&1 | grep -q \"34/34 PASS\" && grep -rq \"tagline\" src/pages/index.astro src/components/ && grep -q \"radius-img\" src/styles/tokens.css", "exit": 0, "duration_s": 14.668, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "a7774fa671522fb695d781169a132c4dfe108f5de2a8fd911e8e2993f16f10e6", "at": "2026-11-01T01:36:15+00:00"}
{"task": "task_0001", "cmd": "test -f public/robots.txt && grep -q \"Sitemap: https://wait4x.online/sitemap-index.xml\" public/robots.txt && grep -q \"Disallow: /admin\" public/robots.txt && grep -q \"SEO 解封操作单\" docs/上线检查单.md", "exit": 0, "duration_s": 0.005, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "59aaab217ae63cfe6ec2718684c60490ecb25a65c0f1a3dd89920e9b7a6795a7", "at": "2026-11-01T01:40:05+00:00"}
{"task": "task_0002", "cmd": "! grep -r \"/en/en/\" dist/ && grep -q 'rel=\"canonical\"' dist/en/archives/mian-sha/index.html && E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs 2>&1 | grep -q \"34/34 PASS\"", "exit": 0, "duration_s": 14.603, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "59aaab217ae63cfe6ec2718684c60490ecb25a65c0f1a3dd89920e9b7a6795a7", "at": "2026-11-01T01:40:19+00:00"}
{"task": "task_0003", "cmd": "node scripts/build-perf/content-lint.mjs dist && bash -n infra/deploy/build.sh && test -x scripts/build-perf/prod-smoke.sh", "exit": 0, "duration_s": 0.071, "stdout_tail_sha256": "209fba8e31f47e3c0984b06e53b6dec06a74dcc59311dae22cca8323af4ad09f", "tree_fp": "59aaab217ae63cfe6ec2718684c60490ecb25a65c0f1a3dd89920e9b7a6795a7", "at": "2026-11-01T01:40:19+00:00"}
{"task": "task_0004", "cmd": "E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs 2>&1 | grep -q \"34/34 PASS\" && grep -q 'data-cmd=\"reader\"' src/components/CommandPalette.astro && grep -q \"html.reader\" src/styles/global.css", "exit": 0, "duration_s": 14.487, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "59aaab217ae63cfe6ec2718684c60490ecb25a65c0f1a3dd89920e9b7a6795a7", "at": "2026-11-01T01:40:34+00:00"}
{"task": "task_0005", "cmd": "E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs 2>&1 | grep -q \"34/34 PASS\" && grep -rqE \"code-win|codebar|窗条\" src/pages/archives/", "exit": 0, "duration_s": 14.324, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "59aaab217ae63cfe6ec2718684c60490ecb25a65c0f1a3dd89920e9b7a6795a7", "at": "2026-11-01T01:40:48+00:00"}
{"task": "task_0006", "cmd": "E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs 2>&1 | grep -q \"34/34 PASS\" && grep -rq \"paper\" src/components/", "exit": 0, "duration_s": 14.598, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "59aaab217ae63cfe6ec2718684c60490ecb25a65c0f1a3dd89920e9b7a6795a7", "at": "2026-11-01T01:41:03+00:00"}
{"task": "task_0007", "cmd": "E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs 2>&1 | grep -q \"34/34 PASS\" && grep -q \"flow-h2-pre\" src/styles/tokens.css && grep -q \"focus-visible\" src/styles/global.css", "exit": 0, "duration_s": 15.156, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "59aaab217ae63cfe6ec2718684c60490ecb25a65c0f1a3dd89920e9b7a6795a7", "at": "2026-11-01T01:41:18+00:00"}
{"task": "task_0008", "cmd": "E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs 2>&1 | grep -q \"34/34 PASS\" && grep -rq \"tagline\" src/pages/index.astro src/components/ && grep -q \"radius-img\" src/styles/tokens.css", "exit": 0, "duration_s": 14.487, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "59aaab217ae63cfe6ec2718684c60490ecb25a65c0f1a3dd89920e9b7a6795a7", "at": "2026-11-01T01:41:32+00:00"}
{"task": "task_0001", "cmd": "test -f public/robots.txt && grep -q \"Sitemap: https://wait4x.online/sitemap-index.xml\" public/robots.txt && grep -q \"Disallow: /admin\" public/robots.txt && grep -q \"SEO 解封操作单\" docs/上线检查单.md", "exit": 0, "duration_s": 0.005, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "5e3805453ecab02ecf7d7db336ac16e05fa30d2b497ba655209e410c8c527dfd", "at": "2026-11-01T01:42:01+00:00"}
{"task": "task_0002", "cmd": "! grep -r \"/en/en/\" dist/ && grep -q 'rel=\"canonical\"' dist/en/archives/mian-sha/index.html && E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs 2>&1 | grep -q \"34/34 PASS\"", "exit": 0, "duration_s": 14.593, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "5e3805453ecab02ecf7d7db336ac16e05fa30d2b497ba655209e410c8c527dfd", "at": "2026-11-01T01:42:16+00:00"}
{"task": "task_0003", "cmd": "node scripts/build-perf/content-lint.mjs dist && bash -n infra/deploy/build.sh && test -x scripts/build-perf/prod-smoke.sh", "exit": 0, "duration_s": 0.058, "stdout_tail_sha256": "209fba8e31f47e3c0984b06e53b6dec06a74dcc59311dae22cca8323af4ad09f", "tree_fp": "5e3805453ecab02ecf7d7db336ac16e05fa30d2b497ba655209e410c8c527dfd", "at": "2026-11-01T01:42:16+00:00"}
{"task": "task_0004", "cmd": "E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs 2>&1 | grep -q \"34/34 PASS\" && grep -q 'data-cmd=\"reader\"' src/components/CommandPalette.astro && grep -q \"html.reader\" src/styles/global.css", "exit": 0, "duration_s": 14.429, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "5e3805453ecab02ecf7d7db336ac16e05fa30d2b497ba655209e410c8c527dfd", "at": "2026-11-01T01:42:31+00:00"}
{"task": "task_0005", "cmd": "E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs 2>&1 | grep -q \"34/34 PASS\" && grep -rqE \"code-win|codebar|窗条\" src/pages/archives/", "exit": 0, "duration_s": 14.396, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "5e3805453ecab02ecf7d7db336ac16e05fa30d2b497ba655209e410c8c527dfd", "at": "2026-11-01T01:42:45+00:00"}
{"task": "task_0006", "cmd": "E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs 2>&1 | grep -q \"34/34 PASS\" && grep -rq \"paper\" src/components/", "exit": 0, "duration_s": 14.486, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "5e3805453ecab02ecf7d7db336ac16e05fa30d2b497ba655209e410c8c527dfd", "at": "2026-11-01T01:42:59+00:00"}
{"task": "task_0007", "cmd": "E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs 2>&1 | grep -q \"34/34 PASS\" && grep -q \"flow-h2-pre\" src/styles/tokens.css && grep -q \"focus-visible\" src/styles/global.css", "exit": 0, "duration_s": 14.479, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "5e3805453ecab02ecf7d7db336ac16e05fa30d2b497ba655209e410c8c527dfd", "at": "2026-11-01T01:43:14+00:00"}
{"task": "task_0008", "cmd": "E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs 2>&1 | grep -q \"34/34 PASS\" && grep -rq \"tagline\" src/pages/index.astro src/components/ && grep -q \"radius-img\" src/styles/tokens.css", "exit": 0, "duration_s": 14.333, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "5e3805453ecab02ecf7d7db336ac16e05fa30d2b497ba655209e410c8c527dfd", "at": "2026-11-01T01:43:28+00:00"}
{"task": "task_0001", "cmd": "test -f public/robots.txt && grep -q \"Sitemap: https://wait4x.online/sitemap-index.xml\" public/robots.txt && grep -q \"Disallow: /admin\" public/robots.txt && grep -q \"SEO 解封操作单\" docs/上线检查单.md", "exit": 0, "duration_s": 0.005, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "a0b992acff0e1c843f7cb94470f4c1304b5e8747a0fa18dea2a93e69200f3de7", "at": "2026-11-01T01:49:52+00:00"}
{"task": "task_0002", "cmd": "! grep -r \"/en/en/\" dist/ && grep -q 'rel=\"canonical\"' dist/en/archives/mian-sha/index.html && E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs 2>&1 | grep -q \"34/34 PASS\"", "exit": 0, "duration_s": 14.485, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "a0b992acff0e1c843f7cb94470f4c1304b5e8747a0fa18dea2a93e69200f3de7", "at": "2026-11-01T01:50:06+00:00"}
{"task": "task_0003", "cmd": "node scripts/build-perf/content-lint.mjs dist && bash -n infra/deploy/build.sh && test -x scripts/build-perf/prod-smoke.sh", "exit": 0, "duration_s": 0.062, "stdout_tail_sha256": "209fba8e31f47e3c0984b06e53b6dec06a74dcc59311dae22cca8323af4ad09f", "tree_fp": "a0b992acff0e1c843f7cb94470f4c1304b5e8747a0fa18dea2a93e69200f3de7", "at": "2026-11-01T01:50:06+00:00"}
{"task": "task_0004", "cmd": "E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs 2>&1 | grep -q \"34/34 PASS\" && grep -q 'data-cmd=\"reader\"' src/components/CommandPalette.astro && grep -q \"html.reader\" src/styles/global.css", "exit": 0, "duration_s": 14.474, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "a0b992acff0e1c843f7cb94470f4c1304b5e8747a0fa18dea2a93e69200f3de7", "at": "2026-11-01T01:50:21+00:00"}
{"task": "task_0005", "cmd": "E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs 2>&1 | grep -q \"34/34 PASS\" && grep -rqE \"code-win|codebar|窗条\" src/pages/archives/", "exit": 0, "duration_s": 14.495, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "a0b992acff0e1c843f7cb94470f4c1304b5e8747a0fa18dea2a93e69200f3de7", "at": "2026-11-01T01:50:35+00:00"}
{"task": "task_0006", "cmd": "E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs 2>&1 | grep -q \"34/34 PASS\" && grep -rq \"paper\" src/components/", "exit": 0, "duration_s": 14.482, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "a0b992acff0e1c843f7cb94470f4c1304b5e8747a0fa18dea2a93e69200f3de7", "at": "2026-11-01T01:50:50+00:00"}
{"task": "task_0007", "cmd": "E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs 2>&1 | grep -q \"34/34 PASS\" && grep -q \"flow-h2-pre\" src/styles/tokens.css && grep -q \"focus-visible\" src/styles/global.css", "exit": 0, "duration_s": 14.59, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "a0b992acff0e1c843f7cb94470f4c1304b5e8747a0fa18dea2a93e69200f3de7", "at": "2026-11-01T01:51:04+00:00"}
{"task": "task_0008", "cmd": "E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs 2>&1 | grep -q \"34/34 PASS\" && grep -rq \"tagline\" src/pages/index.astro src/components/ && grep -q \"radius-img\" src/styles/tokens.css", "exit": 0, "duration_s": 14.452, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "a0b992acff0e1c843f7cb94470f4c1304b5e8747a0fa18dea2a93e69200f3de7", "at": "2026-11-01T01:51:19+00:00"}
@@ -0,0 +1,87 @@
# Phase 5f:SEO 解封与功能优化批 + 美化首批 - 项目范围
<!-- spec-format: 2 -->
## 1. 问题定义
- **项目目标**:①解除生产全站 noindex 封印并补齐 SEO 基建(事故级:上线至今搜索引擎不可见)②修双语暗伤(localePath 双 en/canonical/尾斜杠)③构建期内容 lint+生产冒烟防线 ④reader 纯文本模式 ⑤美化首批 K-Q(流距 token 化/灰阶补档/首屏 tagline/图库质感/微交互/代码块窗条/书评纸面板,全部经视觉模型裁定定案)
- **目标用户**:站长(写作/发布/被搜索到)与读者(阅读质感/双语导航正确性)
- **核心价值**:站点从"隐身可用"到"可被发现+有质感";发布链自带质检
## 2. 假设与待确认
### 2.1 已确认事实
- noindex 来源=/etc/nginx/snippets/esa-origin-ssl.conf:11(我已 curl+grep 实锤);robots.txt 生产 404;ESA 头缓存 30 天
- localePath 在 en→en 场景产出 /en/en/...(404)
- Artalk 评论数端点已实测(GET /api/v2/comments?site_name=wait4x.online&limit=0 → count)
- 美化七问已由 glm-5.3-flash:max 逐图裁定(docs/research/08 §6)
### 2.2 关键假设
- nginx sudo 与 ESA 控制台由用户操作(本包只备好仓库侧与操作单)
- 视觉裁定为终态依据;实现与裁定冲突时以裁定为准并在完成总结记录
### 2.3 待确认问题
- [x] 美化两案之争 → 视觉模型裁定完成(08 文档 §6),无悬案
- [x] 本批范围 → 用户 2026-10-26 全批(A/B/C/E+K-Q;R 热力图暗色与 S 并入 N 后的下一批)
### 2.4 可选解释与取舍
- 纸面板=全边框圆角卡(裁定)优于通栏 hairline——保留标题区外置结构
- 代码块窗条仅 ≥4 行块;三点换灰阶(红线优先于 macOS 梗)
- 图库亮色直贴/暗色 4% 衬底(双模式分判)
### 2.5 已证伪路径
- 首屏 banner 化/大字标语(视觉裁定否决——克制红线)
- 热力图亮色换绿(裁定否决——全页最大色块压灰阶骨架)
- 页面标题 27→22px(裁定否决——层级锚点,要轻减字重不减号)
## 3. 功能范围
### 3.1 核心功能(MVP)
- [x] T1 SEO 仓库侧三件:robots.txt 新建 + /admin 局部 noindex 预案 + 用户操作单(nginx/ESA)
- [x] T2 localePath 双 en 修复 + 自指 canonical + 尾斜杠归一
- [x] T3 构建期内容 lint(断链/图片/frontmatter/双语配对)+ 生产冒烟脚本 + build.sh 守卫挂载
- [x] T4 reader 纯文本模式(html.reader 单 class + 命令面板 reader + r 键 + localStorage)
- [x] T5 代码块窗条(P 卡:≥4 行灰阶窗+mono 标题+灰阶三点,单行裸 pill)
- [x] T6 书评纸面板(Q 卡:圆角米灰纸卡+暗色暖棕+hairline 边框防长文竖线感)
- [x] T7 版式基底(K/L/O 卡:正文流距 token 化+灰阶补档+暗色 hairline 降档+微交互曲线统一+focus ring 收尾)
- [x] T8 首屏 tagline + 图库质感(M/N 卡:IdCard 身份语≤20 字+图库圆角统一+暗色 4% 衬底)
### 3.2 扩展功能
- 无(R 热力图暗色补救列下一批)
### 3.3 不在范围内
- nginx/ESA 实际操作(用户侧);draft 路由 D、评论数 F、EXIF 卡 G、title H(本月批后续包)
## 4. 最小实现路径
- 最简单可行方案:token 值级 CSS 改动为主 + locale.ts 单函数修 + 两个构建脚本;无新依赖
- 暂不引入:blur-up 占位(需管线)、独立设置面板
- 不做的抽象:主题引擎化/样式体系重构(tokens 增量演化)
## 5. 技术决策
- 技术栈:既有 Astro+tokens 体系;lint 用 node 脚本(复用 build-perf 目录与守卫模式)
- 证据协议:evidence: runs
- 本轮允许改动:src/lib/locale.ts、src/layouts/BaseLayout.astro、src/styles/{tokens,global}.css、src/components/{CommandPalette,IdCard*}.astro、src/components/geek/Keyboard.astro、GalleryPage.astro、prose 相关组件、public/robots.txt、scripts/build-perf/、infra/deploy/build.sh、tests/e2e.mjs
- 本轮不应触碰:nginx/ESA(用户)、Header/Footer 结构、sw.js(无涉)、热力图组件
- Git integration branch:`spec/2026-10-26_blog-5f`
### 5.1 验证策略
- 范围级别:package
- 变更对象:SEO 基建文件、locale 工具、构建链 lint 挂载、reader/美化涉及的样式与组件
- 快速检查:`pnpm astro build`
- 集成检查:`node tests/e2e.mjs`(33 项)+ 本包新增断言(canonical/localePath/reader/窗条/纸面板 computed-style)
- 全项目检查:生产 Lighthouse SEO≥95 复测(依赖用户 nginx/ESA 操作单完成)+ 移动溢出密集扫描;仅在 nginx/ESA 面、构建链或 Header 布局再变更时升级范围
- 升级触发:T7 灰阶补档若波及 Header/导航布局,或用户操作单完成后生产复测不达标,升级为全站密集扫描与 LH 多轮中位并记录理由
## 6. 成功标准与验证方式
- [x] dist 每页含自指 canonical 且 en 页无 /en/en/ -> verify: task_0002
- [x] robots.txt 产物存在且含 Sitemap 行 -> verify: task_0001
- [x] 人为断链能让 build 输出 FAIL 清单(warn 一周策略首版即 fail 可议) -> verify: task_0003
- [x] reader 触发后 .bg-glow 隐藏/ICP 可见/刷新保持 -> verify: task_0004
- [x] 多行代码块有窗条与灰阶三点、单行无 -> verify: task_0005
- [x] 书评正文为圆角纸卡(computed border-radius+背景) -> verify: task_0006
- [x] 流距 token 生效(h2 前后距为变量值)+focus ring 统一 -> verify: task_0007
- [x] 首屏 tagline 渲染≤20 字+图库圆角统一 -> verify: task_0008
## 7. 风险与约束
- 灰阶红线:所有美化以 08 §6 裁定为终态依据
- build.sh 引用新脚本守卫先行(车道纪律);lint 首版 fail 可降 warn 一周(发布链安全优先)
- 多车道并行:T2/T3(op-gitea)、T5/T6(op-artalk)、T1/T4/T7/T8(orchestrator);共享文件按 boundary 划界,global.css/tokens 仅 orchestrator 动
@@ -0,0 +1,46 @@
# Phase 5f:SEO 解封与功能优化批 + 美化首批 - 任务拆解
## 使用规则
- boundary/verify 齐备;verify 只读断言;车道工作树开发禁 git,收编由 orchestrator 统一提交
## 阶段一:实现
- [x] T1 SEO 仓库侧三件(robots.txt + admin noindex 预案 + 用户操作单)
- claim: orchestrator
- boundary: 新建 public/robots.txt(Allow 全站;Disallow /admin/ /draft/;Sitemap 行);docs/上线检查单.md 追加「SEO 解封操作单」节(nginx snippet 行号/内部 vhost 补回清单/ESA purge+TTL+绕缓存三动作);nginx/ESA 实操归用户
- verify: `test -f public/robots.txt && grep -q "Sitemap: https://wait4x.online/sitemap-index.xml" public/robots.txt && grep -q "Disallow: /admin" public/robots.txt && grep -q "SEO 解封操作单" docs/上线检查单.md`
- [x] T2 localePath 双 en 修复 + 自指 canonical + 尾斜杠归一
- claim: op-gitea(mesh 车道)
- boundary: src/lib/locale.ts(先归一 zh 再拼前缀);BaseLayout.astro 加自指 canonical(Astro.site+withSlash);Header.astro nav-lang href 尾斜杠补齐;E2E 可加断言
- verify: `! grep -r "/en/en/" dist/ && grep -q 'rel="canonical"' dist/en/archives/mian-sha/index.html && E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs 2>&1 | grep -q "34/34 PASS"`
- [x] T3 构建期内容 lint + 生产冒烟 + build.sh 守卫挂载
- claim: op-gitea(mesh 车道)
- boundary: 新建 scripts/build-perf/content-lint.mjs(dist 断链:站内 a[href] 逐一对产物;图片存在性;zh/en 配对警告)+ scripts/build-perf/prod-smoke.sh(curl robots/sitemap/首页头三断言);build.sh 守卫行(存在性守卫铁律);首版 fail 但提供 SKIP 环境变量逃生门
- verify: `node scripts/build-perf/content-lint.mjs dist && bash -n infra/deploy/build.sh && test -x scripts/build-perf/prod-smoke.sh`
- [x] T4 reader 纯文本模式(html.reader + 命令面板 + r 键 + 持久化)
- claim: orchestrator
- boundary: global.css 尾部集中 html.reader 段(按 08 文档 §4.4 清单:隐 bg-glow/进度条/收 Header 尾部钮/Footer 收单行 ICP 必留;正文字号微升行宽收窄);CommandPalette data-cmd="reader" + ui.ts cmd_reader 双语键;Keyboard r 单键;localStorage 持久化;reduced-motion/无 JS 天然兼容(纯 class)
- verify: `E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs 2>&1 | grep -q "34/34 PASS" && grep -q 'data-cmd="reader"' src/components/CommandPalette.astro && grep -q "html.reader" src/styles/global.css`
- [x] T5 代码块窗条(P 卡:≥4 行灰阶窗+mono 标题+灰阶三点)
- claim: op-artalk(mesh 车道)
- boundary: 技术轨代码块渲染层(自查源码定落点:prose-tech 容器/全局 pre 样式);≥4 行块加窗条(灰阶三点非红黄绿+mono 语言/文件标签),单行块裸 pill;亮暗双态;scoped 样式优先不动 global.css(与 T7 划界)
- verify: `E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs 2>&1 | grep -q "34/34 PASS" && grep -rqE "code-win|codebar|窗条" src/pages/archives/`
- [x] T6 书评纸面板(Q 卡:圆角米灰纸卡+暗色暖棕+hairline 边框)
- claim: op-artalk(mesh 车道)
- boundary: 书评正文容器(prose-reading 所在组件 scoped 样式):圆角纸卡+米灰 #f4f4f2 系亮/暖棕暗态;边框 hairline 级防长文竖线感(08 裁定 §6-2);标题区外置结构不动;--paper 变量需求提给 orchestrator 入 tokens(T7 协同),不直接改 tokens.css
- verify: `E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs 2>&1 | grep -q "34/34 PASS" && grep -rq "paper" src/components/`
- [x] T7 版式基底(K/L/O 卡:流距 token+灰阶补档+暗色 hairline 降档+微交互曲线+focus ring)
- claim: orchestrator(tokens/global 专属车道)
- boundary: tokens.css(--flow-* 流距五变量+灰阶补档评估+--paper 纸色变量);global.css(h2/h3/p+p 改用流距变量、暗色 hairline 降一档、过渡曲线统一 token :where 一段、focus-visible ring 统一);标题字重 700→600(裁定 #6);行距 +2~4px(裁定 #7);移动端密集扫描复测(涉布局面)
- verify: `E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs 2>&1 | grep -q "34/34 PASS" && grep -q "flow-h2-pre" src/styles/tokens.css && grep -q "focus-visible" src/styles/global.css`
- [x] T8 首屏 tagline + 图库质感(M/N 卡:身份语+圆角统一+暗色衬底)
- claim: orchestrator
- boundary: 首页身份区(IdCard 或等位组件)加 tagline ≤20 字(措辞备两案入完成总结供用户选,默认「写作者 · 读者 · 快门按压者」);GalleryPage 圆角统一 --radius-img 暗色 4% 衬底(裁定 §6-4 双模式分判;衬底圆角≤照片圆角防框中框);亮色直贴不动
- verify: `E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs 2>&1 | grep -q "34/34 PASS" && grep -rq "tagline" src/pages/index.astro src/components/ && grep -q "radius-img" src/styles/tokens.css`
@@ -0,0 +1,94 @@
# Phase 5g:页面后台化(blocks 模型)与字体子集化 - 完成总结
## 交付结论
- 结果:完成
- 完成时间:2026-11-02 19:19
## 假设回顾
### 已验证假设
- 块渲染器受控,破版风险低于整页 HTML 直编(方案 10 §2 对比表)
- 字体子集化(pyftsubset 按页实际用字)能把艺术字体单页增量压到 ≤150KB 阈值内
### 仍未完全验证的假设
- 无
## 交付范围
### 已交付
- T1 blocks 骨架(关于页块式页面构建器)
- T2 块型扩充与图片管线
- T3 样式即数据(theme.yaml + 对比度闸门)
- T4 首页细化与去重
- T5 开源页收尾(缓存/回落/告警)
- T6 文档与后台 hint 补齐
- T7 字体子集化工具
### 未交付
- 无
### 偏差说明
- 无
## 简化决策
### 保持简单的关键选择
- 最简单可行方案:块类型先用 3 类跑通闭环(prose/tracks/divider),再扩到 9 类;theme 只做 12 个白名单旋钮
- 暂不引入:可视化块预览、块级动画配置
- 不做的抽象:通用页面 DSL(只服务现有页面)
### 本轮明确不做的内容
- 换 CMS、页面布局的任意拖拽网格、运行时编辑器
## 变更边界
### 本轮主要改动模块
- 见 tasks.md 的 boundary 记录
### 明确未触碰的区域
- 换 CMS、页面布局的任意拖拽网格、运行时编辑器
## 验证证据
### 备注
- 机器证据:runs.jsonl(见验证范围的机器验证率)
## 验证范围
- 级别:package
- 机器验证率:7/7
- 规则:check 与 done 复用该范围;只有记录升级触发时才执行全项目检查
## 哲学生效证据
### 行为成效回填
- Development Record 行为门禁通过
### 一致性门禁回顾
- 验收面(checklist 或成功标准)已通过
## Git 记录
- 日期时间:2026-11-02 19:19
- 范围:Phase 5g:页面后台化(blocks 模型)与字体子集化
- 功能:T1 blocks 骨架(关于页块式页面构建器);T2 块型扩充与图片管线;T3 样式即数据(theme.yaml + 对比度闸门);T4 首页细化与去重;T5 开源页收尾(缓存/回落/告警);T6 文档与后台 hint 补齐;T7 字体子集化工具
- 操作:提交 / 推送
- 成效:Development Record 行为门禁通过
- 提交:pending local commit
- 提交区间:3822e150a87ac5fee56c9b2724eae23389e7c34e..23b79a2473674e75608736e2cac491b163a49c35
- 推送:not-run (push stage follows commit)
## AGENTS.md 增量提议
```diff
Phase 5g 资产:①块式页面构建器(九类 discriminatedUnion + 宽松分支容错:已知块严格校验、未知块 WARN 丢弃不炸构建);②样式即数据(theme.yaml 九组白名单旋钮 + :root:root 特异性注入 + 产物级对比度闸门,负例 1.61:1 被拦);③首页抽 HomePage 消 zh/en 双份;④字体子集化工具(汇文 694→420KB −40%;快乐体实测反向 −9% 故不启用,结论入档避免后人重试);⑤config.yml 重复键结构缺陷的教训——Python yaml 静默覆盖,必须用 StrictLoader/Node yaml 校验,已固化为 build 闸门 config-check.py;⑥并发构建擦 dist 的解法:build→cp -a dist 快照→快照起服务跑 E2E。
```
## 被拒绝的扩展提议
- 无
## 门禁证据
- check_spec_package.py exit 0
## 遗留事项
- 无;问题处置见下方结构化区块
## 问题处置
```json
{
"issues": [],
"version": 1
}
```
@@ -0,0 +1,7 @@
{"task": "task_0001", "cmd": "E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs 2>&1 | grep -q \"37/37 PASS\" && grep -q \"discriminatedUnion\" src/content.config.ts && grep -q \"BlockRenderer\" src/components/pages/AboutPage.astro && grep -q \"typeKey\" public/admin/config.yml", "exit": 0, "duration_s": 18.466, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "7a7e460dbaff3855c2da899e62a1489b19a1d25a4a91ecfb211fadec40bff343", "at": "2026-11-02T11:18:21+00:00"}
{"task": "task_0002", "cmd": "E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs 2>&1 | grep -q \"37/37 PASS\" && ls src/components/blocks/ | grep -qC1 \"Cards\\|Timeline\" && grep -q \"image()\" src/content.config.ts", "exit": 0, "duration_s": 17.177, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "7a7e460dbaff3855c2da899e62a1489b19a1d25a4a91ecfb211fadec40bff343", "at": "2026-11-02T11:18:39+00:00"}
{"task": "task_0003", "cmd": "E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs 2>&1 | grep -q \"37/37 PASS\" && test -f content/site/theme.yaml && node scripts/build-perf/contrast-check.mjs dist", "exit": 0, "duration_s": 17.193, "stdout_tail_sha256": "f66c66c7ce879dad3f789790f8607fbd50e7a5a894e12912a55fd47d082e4f77", "tree_fp": "7a7e460dbaff3855c2da899e62a1489b19a1d25a4a91ecfb211fadec40bff343", "at": "2026-11-02T11:18:56+00:00"}
{"task": "task_0004", "cmd": "E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs 2>&1 | grep -q \"37/37 PASS\" && test -f src/components/pages/HomePage.astro && grep -q \"HomePage\" src/pages/en/index.astro", "exit": 0, "duration_s": 17.189, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "7a7e460dbaff3855c2da899e62a1489b19a1d25a4a91ecfb211fadec40bff343", "at": "2026-11-02T11:19:13+00:00"}
{"task": "task_0005", "cmd": "E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs 2>&1 | grep -q \"37/37 PASS\" && python3 -c \"import json;d=json.load(open('src/data/projects-fallback.json'));assert len(d)>=5\"", "exit": 0, "duration_s": 17.214, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "7a7e460dbaff3855c2da899e62a1489b19a1d25a4a91ecfb211fadec40bff343", "at": "2026-11-02T11:19:30+00:00"}
{"task": "task_0006", "cmd": "test -f docs/管理台.md && grep -q \"blocks\\|区块\" docs/管理台.md && grep -c \"hint\" public/admin/config.yml | awk '$1>=20'", "exit": 0, "duration_s": 0.004, "stdout_tail_sha256": "913f5d1da2feaf4deeccc9e55cbb350a20f12b3f507e87be85dbb77fdd3cb9bc", "tree_fp": "7a7e460dbaff3855c2da899e62a1489b19a1d25a4a91ecfb211fadec40bff343", "at": "2026-11-02T11:19:30+00:00"}
{"task": "task_0007", "cmd": "test -f scripts/build-perf/subset-fonts.py && python3 -c \"import fontTools; print('fonttools ok')\" && E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs 2>&1 | grep -q \"37/37 PASS\"", "exit": 0, "duration_s": 17.19, "stdout_tail_sha256": "3bcefe2ef2e30a5bbe067d31368a9765d7843d0cc24738a60c946c020e581348", "tree_fp": "7a7e460dbaff3855c2da899e62a1489b19a1d25a4a91ecfb211fadec40bff343", "at": "2026-11-02T11:19:47+00:00"}
@@ -0,0 +1,84 @@
# Phase 5g:页面后台化(blocks 模型)与字体子集化 - 项目范围
<!-- spec-format: 2 -->
## 1. 问题定义
- **项目目标**:①按 docs/research/10 方案 E 把关于页改造成「块式页面构建器」(结构可增删排序)②样式即数据(theme.yaml → 构建期 CSS 变量,含对比度闸门)③首页字段细化并抽 HomePage 消 zh/en 双份 ④开源页收尾(README 缓存/回落/告警)⑤后台文档与 hint 补齐 ⑥字体子集化工具(把艺术字体单页增量从数百 KB 降到数十 KB)
- **目标用户**:站长(希望整页可编辑、能上更多艺术字体)与读者(版式与可读性不退化)
- **核心价值**:内容与结构的编辑权交回后台,同时守住灰阶体系/双语/SEO/构建期零运行时四条红线
## 2. 假设与待确认
### 2.1 已确认事实
- Sveltia 0.252 支持 list 的 types + typeKey(方案 10 §3.4 从本仓 bundle 取证),块模型可行
- 页面文案 markdown 净化(D1/D2)已在 2cab243 落地(P0 完成)
- 字体实测:@fontsource CJK 切片粒度导致单页 +163KB~+8605KB;现有 6 自托管族为基线
### 2.2 关键假设
- 块渲染器受控,破版风险低于整页 HTML 直编(方案 10 §2 对比表)
- 字体子集化(pyftsubset 按页实际用字)能把艺术字体单页增量压到 ≤150KB 阈值内
### 2.3 待确认问题
- [x] 关于页整页可编辑路线 → 用户 2026-11-02 批准方案 E(blocks + theme.yaml + 局部自由区)
- [x] 字体扩充路线 → 先落 6 族,其余按子集化工具就绪后再评估(本包 T7 交付工具)
### 2.4 可选解释与取舍
- 关于页保留 intro markdown 通道作为 compat(blocks 为空时三级回落:blocks → intro → 默认文案)
- 样式即数据只开放白名单旋钮(约 12 项),不开放整份 tokens.css
- 图片只走 Astro image() 管线,禁止块内填外链/绝对路径(schema 层拦)
### 2.5 已证伪路径
- 整页富文本直编(C 方案):XSS 面大、无设计兜底,弃
- 新增 webfont 直接上(不子集化):单页 +535KB~+8605KB,超阈值,弃(改走 T7 工具)
## 3. 功能范围
### 3.1 核心功能(MVP)
- [x] T1 blocks 骨架:schema(discriminatedUnion)+ page-blocks.ts + BlockRenderer + prose/tracks/divider 三块 + 后台 types 声明
- [x] T2 块型扩充:cards/timeline/quote/figure/linklist/cta + 图标键白名单 + 图片 image() 管线
- [x] T3 样式即数据:theme.yaml + theme.ts + BaseLayout 内联变量 + 对比度闸门脚本
- [x] T4 首页细化与去重:抽 HomePage.astro 消 zh/en 双份 + 字段细化(projectsLimit/announcement 等)
- [x] T5 开源页收尾:README 缓存策略/超时/回落矩阵 + fallback JSON 回填 + include 未命中 WARN
- [x] T6 文档与 hint:docs/管理台.md(块编辑用法)+ 写作指南扩充 + admin 全字段中文 hint
- [x] T7 字体子集化工具:按页用字裁剪艺术字体为小切片 + build.sh 守卫挂载 + 实测对比
### 3.2 扩展功能
- 无
### 3.3 不在范围内
- 换 CMS、页面布局的任意拖拽网格、运行时编辑器
## 4. 最小实现路径
- 最简单可行方案:块类型先用 3 类跑通闭环(prose/tracks/divider),再扩到 9 类;theme 只做 12 个白名单旋钮
- 暂不引入:可视化块预览、块级动画配置
- 不做的抽象:通用页面 DSL(只服务现有页面)
## 5. 技术决策
- 技术栈:Astro 7.4.4 + zod discriminatedUnion + Sveltia list/types + pyftsubset(fonttools)
- 证据协议:evidence: runs
- 本轮允许改动:src/content.config.ts、src/lib/{page-blocks,theme,site-content}.ts、src/components/blocks/*、src/components/pages/{AboutPage,HomePage}.astro、src/pages/{index,en/index,opensource,en/opensource}.astro、src/layouts/BaseLayout.astro、public/admin/*、content/site/*.yaml、scripts/build-perf/*、infra/deploy/build.sh、docs/*、tests/e2e.mjs
- 本轮不应触碰:nginx/ESA、图库灯箱(GalleryPage/PhotoZoom)、Header/Footer 结构
- Git integration branch:`spec/2026-11-02_blog-5g-pages`
### 5.1 验证策略
- 范围级别:package
- 变更对象:页面数据模型与渲染器、后台配置、样式变量生成、字体产物、文档
- 快速检查:`pnpm astro build`(node26)
- 集成检查:`node tests/e2e.mjs`(37 项,本包预期新增块/主题断言)+ content-lint 0/0/0
- 全项目检查:字体体积账(三页增量)+ 视觉复核(glm-5.3-flash:max,块版式与主题变量双态);仅在构建链或 BaseLayout 结构再变更时升级范围
- 升级触发:若 theme 变量波及全局排版(字体字号全站变化)或 blocks 改动触及首页/文章页结构,升级为全站视觉复核与 E2E 全量并记录理由
## 6. 成功标准与验证方式
- [x] 关于页整页由 yaml blocks 驱动,增删排序生效 -> verify: task_0001
- [x] 九类块全部可渲染且旧内容零回归(三级回落) -> verify: task_0002
- [x] theme.yaml 改值 → 产物 CSS 变量变化,对比度闸门拦截不达标 -> verify: task_0003
- [x] 首页 zh/en 由同一组件渲染且字段可配 -> verify: task_0004
- [x] 开源页 README 失败矩阵与 fallback 告警可用 -> verify: task_0005
- [x] 后台文档与 hint 完整 -> verify: task_0006
- [x] 字体子集化工具把指定字体单页增量压到 ≤150KB -> verify: task_0007
## 7. 风险与约束
- 灰阶体系与双语红线不变;彩色仅正文强调
- 块渲染器未知类型跳过 + WARN,绝不炸构建
- 字体子集化需保证字形覆盖(裁错字会出现豆腐块)→ 断言:子集后页面文本逐字命中
- 多车道并行:共享文件(content.config.ts/admin config/docs)按 boundary 划界,tokens/global 由 orchestrator 或指定单车道
@@ -0,0 +1,42 @@
# Phase 5g:页面后台化(blocks 模型)与字体子集化 - 任务拆解
## 使用规则
- boundary/verify 齐备;verify 只读断言;车道工作树开发禁 git,收编由 orchestrator 统一提交
- 构建一律 `export PATH="$HOME/.local/node/bin:$PATH"`(Node 26)
## 阶段一:实现
- [x] T1 blocks 骨架(关于页块式页面构建器)
- claim: op-gitea
- boundary: src/content.config.ts(siteAbout 加 blocks: z.array(discriminatedUnion))、新增 src/lib/page-blocks.ts(getAboutBlocks:过滤 enabled、未知 type 跳过+WARN、文本 pickBi 回落、块数≤24/单块≤4000 字)、新增 src/components/blocks/{BlockRenderer,Prose,Tracks,Divider}.astro、AboutPage.astro 改由 blocks 驱动(blocks 空→intro markdown→默认文案三级回落)、public/admin/config.yml 加 list+types(typeKey: type)
- verify: `E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs 2>&1 | grep -q "37/37 PASS" && grep -q "discriminatedUnion" src/content.config.ts && grep -q "BlockRenderer" src/components/pages/AboutPage.astro && grep -q "typeKey" public/admin/config.yml`
- [x] T2 块型扩充与图片管线
- claim: op-gitea
- boundary: 新增 src/components/blocks/{Cards,Timeline,Quote,Figure,LinkList,Cta}.astro + 图标键白名单(复用 SocialLinks 注册表或新建 icons 注册表)、schema 块型补全(图片字段用 image(),禁止外链字符串)、content/site/assets/ 目录、near docs 图表说明
- verify: `E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs 2>&1 | grep -q "37/37 PASS" && ls src/components/blocks/ | grep -qC1 "Cards\|Timeline" && grep -q "image()" src/content.config.ts`
- [x] T3 样式即数据(theme.yaml + 对比度闸门)
- claim: op-artalk
- boundary: 新增 content/site/theme.yaml(12 个白名单旋钮:prose-size/measure/radius/accent/leading 等)、新增 src/lib/theme.ts(getTheme + themeVarsCss 生成 :root:root{…})、BaseLayout.astro head 内联 theme 变量(构建期静态)、新增 scripts/build-perf/contrast-check.mjs(断言 theme 结果下正文对比度 ≥4.5)、public/admin/config.yml 加 theme 单例、build.sh 守卫挂载
- verify: `E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs 2>&1 | grep -q "37/37 PASS" && test -f content/site/theme.yaml && node scripts/build-perf/contrast-check.mjs dist`
- [x] T4 首页细化与去重
- claim: op-artalk
- boundary: 新增 src/components/pages/HomePage.astro(承接现 index.astro 区块逻辑,locale prop)、src/pages/{index,en/index}.astro 改为薄壳、home.yaml 增 projectsLimit / announcement(markdown)/ postsTracks(轨开关细分),site-content 访问器同步
- verify: `E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs 2>&1 | grep -q "37/37 PASS" && test -f src/components/pages/HomePage.astro && grep -q "HomePage" src/pages/en/index.astro`
- [x] T5 开源页收尾(缓存/回落/告警)
- claim: worker-one
- boundary: src/lib/site-content.ts(README 缓存策略与超时、回落矩阵)、content/.github-readme.md 与 README HTML 剥离策略(复用 rehype-content-sanitize 已挡危险标签;补: 外链徽章不阻断)、src/data/projects-fallback.json 回填 5-8 条真实仓、projects.ts include 未命中 console.warn
- verify: `E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs 2>&1 | grep -q "37/37 PASS" && python3 -c "import json;d=json.load(open('src/data/projects-fallback.json'));assert len(d)>=5"`
- [x] T6 文档与后台 hint 补齐
- claim: worker-one
- boundary: docs/管理台.md(块编辑怎么用、增删排序、预览限制)、docs/写作指南.md(块字段表/受限类/主题项)、public/admin/config.yml 全字段中文 hint(含 T1-T4 新增字段)
- verify: `test -f docs/管理台.md && grep -q "blocks\|区块" docs/管理台.md && grep -c "hint" public/admin/config.yml | awk '$1>=20'`
- [x] T7 字体子集化工具
- claim: orchestrator
- boundary: 新增 scripts/build-perf/subset-fonts.py(pyftsubset 按指定字体 + 页面实际用字生成子集 woff2 + 重写 fonts.css 指向子集文件)、infra/deploy/build.sh 守卫挂载、docs/写作指南加字流程补"子集化"一节
- verify: `test -f scripts/build-perf/subset-fonts.py && python3 -c "import fontTools; print('fonttools ok')" && E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs 2>&1 | grep -q "37/37 PASS"`
@@ -0,0 +1,95 @@
# Phase 5h:内容表达能力与社交反馈 - 完成总结
## 交付结论
- 结果:完成
- 完成时间:2026-11-02 21:16
## 假设回顾
### 已验证假设
- 瞬间富文本走与全站文案同一条净化管线即可满足「字体/颜色可换」且不引入 XSS 面(危险标签整棵丢弃、协议白名单 http/https/mailto)
- 每条瞬间一个 Artalk page_key(`/moments/<slug>/`)不会与页面级 pv 冲突;Artalk 实例按需挂载(展开才 init)可避免 "one page one Artalk" 的全局实例冲突
- 开源页可用本地数据(已有 repo 元数据 + GitHub GraphQL 拉取)自绘 followers/stars/语言占比,无需任何第三方徽章服务
### 仍未完全验证的假设
- 无
## 交付范围
### 已交付
- T1 文章置顶(契约 + 排序 + 标记 + 后台)
- T2 瞬间富文本(字体可换 / 颜色可换 / 多形式)
- T3 瞬间逐条评论(Artalk 独立 page_key + 折叠懒挂载)
- T4 计数徽标(眼睛图标 = 浏览数,气泡图标 = 评论数)
- T5 开源页重构(原生 GitHub 档案卡,去外链图依赖)
- T6 关于页承接 README 的个人叙述
- T7 写作模板示例文章 + 文档与 hint 收尾
### 未交付
- 无
### 偏差说明
- 无
## 简化决策
### 保持简单的关键选择
- 最简单可行方案:置顶先用 boolean + 排序键;瞬间富文本直接把 `text` 交给既有 `renderRich`;评论用现有 Artalk 组件克隆一份带折叠的;计数徽标只做 pv + count 两个数
- 暂不引入:置顶权重/多级置顶、瞬间的图片轮播、评论楼中楼定制、开源页的实时 Star 曲线
- 不做的抽象:通用「内容社交层」框架(只服务现有页面)
### 本轮明确不做的内容
- 评论回复通知/邮件推送、点赞/投票、阅读进度、站点级总访问量统计(Artalk 无匿名聚合端点)
## 变更边界
### 本轮主要改动模块
- 见 tasks.md 的 boundary 记录
### 明确未触碰的区域
- 评论回复通知/邮件推送、点赞/投票、阅读进度、站点级总访问量统计(Artalk 无匿名聚合端点)
## 验证证据
### 备注
- 机器证据:runs.jsonl(见验证范围的机器验证率)
## 验证范围
- 级别:package
- 机器验证率:7/7
- 规则:check 与 done 复用该范围;只有记录升级触发时才执行全项目检查
## 哲学生效证据
### 行为成效回填
- Development Record 行为门禁通过
### 一致性门禁回顾
- 验收面(checklist 或成功标准)已通过
## Git 记录
- 日期时间:2026-11-02 21:16
- 范围:Phase 5h:内容表达能力与社交反馈
- 功能:T1 文章置顶(契约 + 排序 + 标记 + 后台);T2 瞬间富文本(字体可换 / 颜色可换 / 多形式);T3 瞬间逐条评论(Artalk 独立 page_key + 折叠懒挂载);T4 计数徽标(眼睛图标 = 浏览数,气泡图标 = 评论数);T5 开源页重构(原生 GitHub 档案卡,去外链图依赖);T6 关于页承接 README 的个人叙述;T7 写作模板示例文章 + 文档与 hint 收尾
- 操作:提交 / 推送
- 成效:Development Record 行为门禁通过
- 提交:pending local commit
- 提交区间:ed473e996a3707f98015a40e4388aa4fa58b79ac..bdaf02f9cae774e2a094eb716a95be3f0eddab56
- 推送:not-run (push stage follows commit)
## AGENTS.md 增量提议
```diff
Phase 5h 资产:①置顶契约(sortPosts 统一四处排序,只改顺序不动 pubDate/URL/RSS)②瞬间富文本(复用 renderRich 净化链,危险标签整棵丢弃)③瞬间逐条评论(展开才挂 Artalk;评论键用虚拟路径 /moments/<slug>/——实测 # 在查询串会被截断成页面级键,导致所有瞬间共用一个评论流)④计数徽标 ViewMeta(眼睛/气泡、零第三方图片;修同载双计与同页数字差 1 两个自伤 bug,口径=「请求次数对」不等于「展示自洽」)⑤开源页去 README 外链依赖(7 类第三方图与 align 居中失效是「没居中/图片不显示」的根因)⑥灰阶纪律清剿:项目墙 15 个彩色语言点、并列 36% 单着色、全局 .tap 泄漏致卡片居中 ⑦视觉验收新增两条可复用断言法:Range.getClientRects 判折行、scrollWidth==innerWidth 判横向溢出。
```
## 被拒绝的扩展提议
- 无
## 门禁证据
- check_spec_package.py exit 0
## 遗留事项
- 无;问题处置见下方结构化区块
## 问题处置
```json
{
"issues": [],
"version": 1
}
```
@@ -0,0 +1,28 @@
{"task": "task_0001", "cmd": "E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs 2>&1 | grep -qE \"汇总: [0-9]+/[0-9]+ PASS\" && grep -q \"pinned\" src/content.config.ts && grep -q \"pinned\" src/components/PostList.astro && grep -q \"pinned\" public/admin/config.yml", "exit": 0, "duration_s": 21.239, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "a4c5572a52c4f546900c03b86c7cfd32056ad32ec27194018ffc175ac700f3c5", "at": "2026-11-02T12:58:45+00:00"}
{"task": "task_0002", "cmd": "E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs 2>&1 | grep -qE \"汇总: [0-9]+/[0-9]+ PASS\" && grep -qE \"renderRich|renderMarkdown\" src/components/pages/MomentsPage.astro && python3 -c \"import yaml;d=yaml.safe_load(open('public/admin/config.yml'));m=[c for c in d['collections'] if c['name']=='moments'][0];t=[f for f in m['fields'] if f['name']=='text'][0];assert t['widget']=='markdown',t\"", "exit": 0, "duration_s": 21.893, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "a4c5572a52c4f546900c03b86c7cfd32056ad32ec27194018ffc175ac700f3c5", "at": "2026-11-02T12:59:07+00:00"}
{"task": "task_0003", "cmd": "test -f src/components/MomentComments.astro && grep -q \"MomentComments\" src/components/pages/MomentsPage.astro && grep -q \"moments/#\" src/components/MomentComments.astro && E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs 2>&1 | grep -qE \"汇总: [0-9]+/[0-9]+ PASS\"", "exit": 0, "duration_s": 21.073, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "a4c5572a52c4f546900c03b86c7cfd32056ad32ec27194018ffc175ac700f3c5", "at": "2026-11-02T12:59:28+00:00"}
{"task": "task_0004", "cmd": "test -f src/components/geek/ViewMeta.astro && grep -q \"ViewMeta\" src/components/ArtalkComments.astro && grep -q \"ViewMeta\" src/pages/archives/[...slug].astro && E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs 2>&1 | grep -qE \"汇总: [0-9]+/[0-9]+ PASS\"", "exit": 0, "duration_s": 23.015, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "a4c5572a52c4f546900c03b86c7cfd32056ad32ec27194018ffc175ac700f3c5", "at": "2026-11-02T12:59:51+00:00"}
{"task": "task_0005", "cmd": "grep -q \"GithubProfile\" src/pages/opensource.astro && python3 -c \"import re,pathlib;p=pathlib.Path('dist/opensource/index.html').read_text();bad=[u for u in ['img.shields.io','readme-typing-svg','github-readme-stats','streak-stats','komarev','github-readme-activity-graph'] if u in p];assert not bad,bad\"", "exit": 0, "duration_s": 0.03, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "a4c5572a52c4f546900c03b86c7cfd32056ad32ec27194018ffc175ac700f3c5", "at": "2026-11-02T12:59:52+00:00"}
{"task": "task_0006", "cmd": "python3 -c \"import yaml;d=yaml.safe_load(open('content/site/about.yaml'));types=[b['type'] for b in d.get('blocks') or []];assert 'prose' in types and ('linklist' in types or 'cards' in types),types\" && E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs 2>&1 | grep -qE \"汇总: [0-9]+/[0-9]+ PASS\"", "exit": 0, "duration_s": 21.067, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "a4c5572a52c4f546900c03b86c7cfd32056ad32ec27194018ffc175ac700f3c5", "at": "2026-11-02T13:00:13+00:00"}
{"task": "task_0007", "cmd": "test -f content/posts/writing-template.md && test -f content/posts-en/writing-template.md && grep -q \"pinned: true\" content/posts/writing-template.md && grep -qE \"font-kai|c-blue\" content/posts/writing-template.md && grep -q \"写作模板\" docs/写作指南.md", "exit": 0, "duration_s": 0.005, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "a4c5572a52c4f546900c03b86c7cfd32056ad32ec27194018ffc175ac700f3c5", "at": "2026-11-02T13:00:13+00:00"}
{"task": "task_0001", "cmd": "E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs 2>&1 | grep -qE \"汇总: [0-9]+/[0-9]+ PASS\" && grep -q \"pinned\" src/content.config.ts && grep -q \"pinned\" src/components/PostList.astro && grep -q \"pinned\" public/admin/config.yml", "exit": 0, "duration_s": 20.732, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "f35452f48bd7d96babf34565aeef8d17c25191be02e07d93fd27922795604348", "at": "2026-11-02T13:04:09+00:00"}
{"task": "task_0002", "cmd": "E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs 2>&1 | grep -qE \"汇总: [0-9]+/[0-9]+ PASS\" && grep -qE \"renderRich|renderMarkdown\" src/components/pages/MomentsPage.astro && python3 -c \"import yaml;d=yaml.safe_load(open('public/admin/config.yml'));m=[c for c in d['collections'] if c['name']=='moments'][0];t=[f for f in m['fields'] if f['name']=='text'][0];assert t['widget']=='markdown',t\"", "exit": 0, "duration_s": 20.695, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "f35452f48bd7d96babf34565aeef8d17c25191be02e07d93fd27922795604348", "at": "2026-11-02T13:04:30+00:00"}
{"task": "task_0003", "cmd": "test -f src/components/MomentComments.astro && grep -q \"MomentComments\" src/components/pages/MomentsPage.astro && grep -q 'moments/${slug}/' src/components/MomentComments.astro && E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs 2>&1 | grep -qE \"汇总: [0-9]+/[0-9]+ PASS\"", "exit": 0, "duration_s": 20.501, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "f35452f48bd7d96babf34565aeef8d17c25191be02e07d93fd27922795604348", "at": "2026-11-02T13:04:50+00:00"}
{"task": "task_0004", "cmd": "test -f src/components/geek/ViewMeta.astro && grep -q \"ViewMeta\" src/components/ArtalkComments.astro && grep -q \"ViewMeta\" src/pages/archives/[...slug].astro && E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs 2>&1 | grep -qE \"汇总: [0-9]+/[0-9]+ PASS\"", "exit": 0, "duration_s": 20.83, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "f35452f48bd7d96babf34565aeef8d17c25191be02e07d93fd27922795604348", "at": "2026-11-02T13:05:11+00:00"}
{"task": "task_0005", "cmd": "grep -q \"GithubProfile\" src/pages/opensource.astro && python3 -c \"import re,pathlib;p=pathlib.Path('dist/opensource/index.html').read_text();bad=[u for u in ['img.shields.io','readme-typing-svg','github-readme-stats','streak-stats','komarev','github-readme-activity-graph'] if u in p];assert not bad,bad\"", "exit": 0, "duration_s": 0.029, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "f35452f48bd7d96babf34565aeef8d17c25191be02e07d93fd27922795604348", "at": "2026-11-02T13:05:11+00:00"}
{"task": "task_0006", "cmd": "python3 -c \"import yaml;d=yaml.safe_load(open('content/site/about.yaml'));types=[b['type'] for b in d.get('blocks') or []];assert 'prose' in types and ('linklist' in types or 'cards' in types),types\" && E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs 2>&1 | grep -qE \"汇总: [0-9]+/[0-9]+ PASS\"", "exit": 0, "duration_s": 21.719, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "f35452f48bd7d96babf34565aeef8d17c25191be02e07d93fd27922795604348", "at": "2026-11-02T13:05:33+00:00"}
{"task": "task_0007", "cmd": "test -f content/posts/writing-template.md && test -f content/posts-en/writing-template.md && grep -q \"pinned: true\" content/posts/writing-template.md && grep -qE \"font-kai|c-blue\" content/posts/writing-template.md && grep -q \"写作模板\" docs/写作指南.md", "exit": 0, "duration_s": 0.005, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "f35452f48bd7d96babf34565aeef8d17c25191be02e07d93fd27922795604348", "at": "2026-11-02T13:05:33+00:00"}
{"task": "task_0001", "cmd": "E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs 2>&1 | grep -qE \"汇总: [0-9]+/[0-9]+ PASS\" && grep -q \"pinned\" src/content.config.ts && grep -q \"pinned\" src/components/PostList.astro && grep -q \"pinned\" public/admin/config.yml", "exit": 0, "duration_s": 21.074, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "62086ce4cccd8c576913526a7ae517471278660c8e6cc6d7e24f9ea632038195", "at": "2026-11-02T13:12:48+00:00"}
{"task": "task_0002", "cmd": "E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs 2>&1 | grep -qE \"汇总: [0-9]+/[0-9]+ PASS\" && grep -qE \"renderRich|renderMarkdown\" src/components/pages/MomentsPage.astro && python3 -c \"import yaml;d=yaml.safe_load(open('public/admin/config.yml'));m=[c for c in d['collections'] if c['name']=='moments'][0];t=[f for f in m['fields'] if f['name']=='text'][0];assert t['widget']=='markdown',t\"", "exit": 0, "duration_s": 20.656, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "62086ce4cccd8c576913526a7ae517471278660c8e6cc6d7e24f9ea632038195", "at": "2026-11-02T13:13:09+00:00"}
{"task": "task_0003", "cmd": "test -f src/components/MomentComments.astro && grep -q \"MomentComments\" src/components/pages/MomentsPage.astro && grep -q 'moments/${slug}/' src/components/MomentComments.astro && E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs 2>&1 | grep -qE \"汇总: [0-9]+/[0-9]+ PASS\"", "exit": 0, "duration_s": 20.386, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "62086ce4cccd8c576913526a7ae517471278660c8e6cc6d7e24f9ea632038195", "at": "2026-11-02T13:13:29+00:00"}
{"task": "task_0004", "cmd": "test -f src/components/geek/ViewMeta.astro && grep -q \"ViewMeta\" src/components/ArtalkComments.astro && grep -q \"ViewMeta\" src/pages/archives/[...slug].astro && E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs 2>&1 | grep -qE \"汇总: [0-9]+/[0-9]+ PASS\"", "exit": 0, "duration_s": 20.357, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "62086ce4cccd8c576913526a7ae517471278660c8e6cc6d7e24f9ea632038195", "at": "2026-11-02T13:13:49+00:00"}
{"task": "task_0005", "cmd": "grep -q \"GithubProfile\" src/pages/opensource.astro && python3 -c \"import re,pathlib;p=pathlib.Path('dist/opensource/index.html').read_text();bad=[u for u in ['img.shields.io','readme-typing-svg','github-readme-stats','streak-stats','komarev','github-readme-activity-graph'] if u in p];assert not bad,bad\"", "exit": 0, "duration_s": 0.029, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "62086ce4cccd8c576913526a7ae517471278660c8e6cc6d7e24f9ea632038195", "at": "2026-11-02T13:13:49+00:00"}
{"task": "task_0006", "cmd": "python3 -c \"import yaml;d=yaml.safe_load(open('content/site/about.yaml'));types=[b['type'] for b in d.get('blocks') or []];assert 'prose' in types and ('linklist' in types or 'cards' in types),types\" && E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs 2>&1 | grep -qE \"汇总: [0-9]+/[0-9]+ PASS\"", "exit": 0, "duration_s": 21.589, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "62086ce4cccd8c576913526a7ae517471278660c8e6cc6d7e24f9ea632038195", "at": "2026-11-02T13:14:11+00:00"}
{"task": "task_0007", "cmd": "test -f content/posts/writing-template.md && test -f content/posts-en/writing-template.md && grep -q \"pinned: true\" content/posts/writing-template.md && grep -qE \"font-kai|c-blue\" content/posts/writing-template.md && grep -q \"写作模板\" docs/写作指南.md", "exit": 0, "duration_s": 0.005, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "62086ce4cccd8c576913526a7ae517471278660c8e6cc6d7e24f9ea632038195", "at": "2026-11-02T13:14:11+00:00"}
{"task": "task_0001", "cmd": "E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs 2>&1 | grep -qE \"汇总: [0-9]+/[0-9]+ PASS\" && grep -q \"pinned\" src/content.config.ts && grep -q \"pinned\" src/components/PostList.astro && grep -q \"pinned\" public/admin/config.yml", "exit": 0, "duration_s": 20.396, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "4af92016715e63fb205156cc420c1d1a9f264ede8fd6bb358ffc8099741b2cef", "at": "2026-11-02T13:14:57+00:00"}
{"task": "task_0002", "cmd": "E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs 2>&1 | grep -qE \"汇总: [0-9]+/[0-9]+ PASS\" && grep -qE \"renderRich|renderMarkdown\" src/components/pages/MomentsPage.astro && python3 -c \"import yaml;d=yaml.safe_load(open('public/admin/config.yml'));m=[c for c in d['collections'] if c['name']=='moments'][0];t=[f for f in m['fields'] if f['name']=='text'][0];assert t['widget']=='markdown',t\"", "exit": 0, "duration_s": 21.377, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "4af92016715e63fb205156cc420c1d1a9f264ede8fd6bb358ffc8099741b2cef", "at": "2026-11-02T13:15:19+00:00"}
{"task": "task_0003", "cmd": "test -f src/components/MomentComments.astro && grep -q \"MomentComments\" src/components/pages/MomentsPage.astro && grep -q 'moments/${slug}/' src/components/MomentComments.astro && E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs 2>&1 | grep -qE \"汇总: [0-9]+/[0-9]+ PASS\"", "exit": 0, "duration_s": 20.908, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "4af92016715e63fb205156cc420c1d1a9f264ede8fd6bb358ffc8099741b2cef", "at": "2026-11-02T13:15:40+00:00"}
{"task": "task_0004", "cmd": "test -f src/components/geek/ViewMeta.astro && grep -q \"ViewMeta\" src/components/ArtalkComments.astro && grep -q \"ViewMeta\" src/pages/archives/[...slug].astro && E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs 2>&1 | grep -qE \"汇总: [0-9]+/[0-9]+ PASS\"", "exit": 0, "duration_s": 21.893, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "4af92016715e63fb205156cc420c1d1a9f264ede8fd6bb358ffc8099741b2cef", "at": "2026-11-02T13:16:02+00:00"}
{"task": "task_0005", "cmd": "grep -q \"GithubProfile\" src/pages/opensource.astro && python3 -c \"import re,pathlib;p=pathlib.Path('dist/opensource/index.html').read_text();bad=[u for u in ['img.shields.io','readme-typing-svg','github-readme-stats','streak-stats','komarev','github-readme-activity-graph'] if u in p];assert not bad,bad\"", "exit": 0, "duration_s": 0.029, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "4af92016715e63fb205156cc420c1d1a9f264ede8fd6bb358ffc8099741b2cef", "at": "2026-11-02T13:16:02+00:00"}
{"task": "task_0006", "cmd": "python3 -c \"import yaml;d=yaml.safe_load(open('content/site/about.yaml'));types=[b['type'] for b in d.get('blocks') or []];assert 'prose' in types and ('linklist' in types or 'cards' in types),types\" && E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs 2>&1 | grep -qE \"汇总: [0-9]+/[0-9]+ PASS\"", "exit": 0, "duration_s": 20.399, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "4af92016715e63fb205156cc420c1d1a9f264ede8fd6bb358ffc8099741b2cef", "at": "2026-11-02T13:16:22+00:00"}
{"task": "task_0007", "cmd": "test -f content/posts/writing-template.md && test -f content/posts-en/writing-template.md && grep -q \"pinned: true\" content/posts/writing-template.md && grep -qE \"font-kai|c-blue\" content/posts/writing-template.md && grep -q \"写作模板\" docs/写作指南.md", "exit": 0, "duration_s": 0.005, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "4af92016715e63fb205156cc420c1d1a9f264ede8fd6bb358ffc8099741b2cef", "at": "2026-11-02T13:16:22+00:00"}
@@ -0,0 +1,91 @@
# Phase 5h:内容表达能力与社交反馈 - 项目范围
<!-- spec-format: 2 -->
## 1. 问题定义
- **项目目标**:①文章支持置顶(作者指定优先)②瞬间的文字与文章同级表达力(字体可换、颜色可换、多段落/列表/强调)③瞬间每条可独立评论 ④浏览量与评论数用统一的「眼睛/气泡」计数徽标呈现(对齐开源页 GitHub 档案的眼图标语汇)⑤开源页开篇从 GitHub README 直接移植导致:`<div align="center">` 内联对齐失效、shields/vercel/snake/komarev 等外链图在慢网下空白破版、README 的「About Me」与开源页主题无关——重构为原生组件并从 README 解耦 ⑥关于页承接读完 README 后仍想保留的个人叙述 ⑦新增一篇「写作模板」示例文章,把可用排版能力一次演示完
- **目标用户**:站长(写得更自由,少打字面 HTML)、读者(读到更丰富且不破版的页)
- **核心价值**:内容表达力与后台编辑力对上,同时把外链依赖与版式事故面砍掉;社交反馈(浏览/评论)在灰阶体系内可量化可见
## 2. 假设与待确认
### 2.1 已确认事实
- Artalk v2 匿名可读:`GET /artalk/api/v2/comments?site_name=…&page_key=…&limit=1` 返回 `{count, roots_count, page:{pv}}`(生产实测,2026-11-02);`POST /artalk/api/v2/pages/pv` 返回 `{pv}`(现有组件在用)
- 富文本管线已存在且已净化:`renderRich`(markdown + rehype-content-sanitize 标签/属性/协议三层白名单)与 `rehype-class-whitelist`(35 项排版类,单一事实源 `public/admin/typography-classes.js`)
- 现有 6 自托管字族(含 font-huiwen 子集化 420KB 单页)与 16 彩色语义类 + 4 灰阶语义类
- 开源页 README 现由 `intro_from: "off"` 关闭,缓存文件 `content/.github-readme.md`(3138B)含 6 类外链图(readme-typing-svg / shields.io / github-readme-stats / streak-stats / activity-graph / snake / komarev)
- 首页/归档/书架三处列表排序均为 `pubDate` 降序,无置顶概念(`grep pinned` 只命中开源页仓库白名单)
### 2.2 关键假设
- 瞬间富文本走与全站文案同一条净化管线即可满足「字体/颜色可换」且不引入 XSS 面(危险标签整棵丢弃、协议白名单 http/https/mailto)
- 每条瞬间一个 Artalk page_key(`/moments/<slug>/`)不会与页面级 pv 冲突;Artalk 实例按需挂载(展开才 init)可避免 "one page one Artalk" 的全局实例冲突
- 开源页可用本地数据(已有 repo 元数据 + GitHub GraphQL 拉取)自绘 followers/stars/语言占比,无需任何第三方徽章服务
### 2.3 待确认问题
- [x] 瞬间文字是否允许 HTML 类名(字体/颜色)→ 用户原话「字体可换、颜色可换等」,按富文本管线放行 35 项白名单类
- [x] 开源页要不要与 README 完全一致 → 用户原话「可以自行进行美化,无需完全一致」,按原生组件重构
- [x] About Me 类内容放哪 → 用户原话「有些可以直接移植到关于页面中」,迁到 about.yaml blocks
### 2.4 可选解释与取舍
- 置顶只作用于列表排序与视觉标记,不改 URL / 不改 pubDate 语义(订阅源按时间仍正确)
- 计数徽标一次只拉当前页的 pv(POST 计一次)与评论数(GET 读);瞬间列表按可视区懒拉,避免 N 次请求把首屏拖慢
- 开源页保留 `intro_from` 开关作为 compat,但默认 off 且文档标注「不推荐」;README 渲染通道不删除(后台仍可切回)
### 2.5 已证伪路径
- 直接渲染 README HTML 并试图用 CSS 修 `align="center"`:内联属性与 sanitize 的交互不可控,且外链图仍会空白破版 → 弃,改原生组件
- 每条瞬间都 init 一个 Artalk 实例常驻:内存与请求成本高、且 Artalk 对多实例的支持未验证 → 改为展开时挂载
- 用第三方徽章服务显示访客数(komarev 等):外链依赖 + 灰阶体系外颜色 → 弃,用 Artalk pv 自绘
## 3. 功能范围
### 3.1 核心功能(MVP)
- [x] T1 文章置顶:posts/postsEn schema `pinned`、四处列表排序(首页双轨/归档/书架)、视觉标记(灰阶 pin)、后台开关
- [x] T2 瞬间富文本:moments `text` 支持 markdown + 白名单排版类(字体/颜色/强调/多段/列表)、净化管线复用、后台改 markdown 编辑器、旧纯文本零回归
- [x] T3 瞬间逐条评论:每条 moment 独立 page_key 的 Artalk 评论区(折叠、展开时挂载)、评论数联动、双语页共用同一 key
- [x] T4 计数徽标:新增 `ViewMeta` 组件(眼睛图标 = 浏览数、气泡图标 = 评论数),接入文章页/瞬间页/关于页/开源页
- [x] T5 开源页重构:原生 GitHub 档案卡(头像/名字/关注者/星标/语言占比,本地数据自绘)替换 README 外链图;`align` 失效与破图问题消失;README 通道降级为可选
- [x] T6 关于页承接:把 README 的 About Me / Find Me 迁为 about.yaml blocks(prose + linklist),开源页只留开源相关
- [x] T7 写作模板示例:中英双语示例文章,演示标题层级/彩色强调/字族/引用/代码/图/表/链接卡片/折叠,并作为置顶功能的活样本;docs 与后台 hint 同步
### 3.2 扩展功能
- 无
### 3.3 不在范围内
- 评论回复通知/邮件推送、点赞/投票、阅读进度、站点级总访问量统计(Artalk 无匿名聚合端点)
## 4. 最小实现路径
- 最简单可行方案:置顶先用 boolean + 排序键;瞬间富文本直接把 `text` 交给既有 `renderRich`;评论用现有 Artalk 组件克隆一份带折叠的;计数徽标只做 pv + count 两个数
- 暂不引入:置顶权重/多级置顶、瞬间的图片轮播、评论楼中楼定制、开源页的实时 Star 曲线
- 不做的抽象:通用「内容社交层」框架(只服务现有页面)
## 5. 技术决策
- 技术栈:Astro 7.4.4 + zod schema + Artalk v2 HTTP API + GitHub GraphQL(已有 token)
- 证据协议:evidence: runs
- 本轮允许改动:src/content.config.ts、src/components/{PostList,ArtalkComments}.astro、src/components/pages/{HomePage,MomentsPage,BooksPage}.astro、src/components/geek/{ViewMeta,GithubProfile}.astro(新)、src/pages/archives/[...slug].astro、src/pages/en/archives/[...slug].astro、src/pages/{about,opensource,en/about,en/opensource}.astro、src/lib/{projects,site-content,heatmap}.ts、content/site/{about,opensource}.yaml、content/posts/*(新增示例文章)、content/posts-en/*、public/admin/{config.yml,typography-classes.js}、docs/*、tests/e2e.mjs
- 本轮不应触碰:nginx/ESA、灯箱与图库(GalleryPage/PhotoZoom)、Header/Footer 结构、构建链顺序、tokens.css/global.css 基础尺度
- Git integration branch:`spec/2026-11-02_blog-5h-content-social`
### 5.1 验证策略
- 范围级别:package
- 变更对象:内容模型(置顶/瞬间富文本)、渲染组件(计数/评论/档案卡)、后台配置、示例内容
- 快速检查:`pnpm astro build`(node26)+ `python3 scripts/build-perf/config-check.py .`
- 集成检查:`E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs`(37 项,本包新增置顶顺序/徽标存在/瞬间富文本断言)+ content-lint 0/0/0
- 全项目检查:生产实测四条(开源页零第三方外链图、瞬间评论可发可见、徽标数值与 Artalk 端点对账一致、关于页承接内容渲染);**升级条件**——仅当改动触及 BaseLayout/全局样式(tokens/字体栈)或 Artalk 主题接管层时才执行,本包因往 tokens 补 emoji 回退链已触发一次并记录了理由
- 升级触发:若动到 BaseLayout/全局样式或 Artalk 主题接管层,升级为全站视觉复核(glm-5.3-flash:max,≤6 图/轮)
## 6. 成功标准与验证方式
- [x] 置顶文章排在列表首位且带可见标记,取消置顶后恢复时间序 -> verify: task_0001
- [x] 瞬间文字可用字体类与彩色强调,危险标签被净化丢弃,旧纯文本渲染不变 -> verify: task_0002
- [x] 每条瞬间可展开评论区并成功发帖,双语页共用同一评论流 -> verify: task_0003
- [x] 文章/瞬间/关于/开源四页显示「浏览数 + 评论数」徽标且数值与 Artalk 一致 -> verify: task_0004
- [x] 开源页零第三方图片请求,档案卡数据来自本地/GraphQL,居中与破图问题不再出现 -> verify: task_0005
- [x] README 的个人叙述出现在关于页,开源页只保留开源相关内容 -> verify: task_0006
- [x] 示例文章渲染全部演示能力且构建通过,docs/hint 同步 -> verify: task_0007
## 7. 风险与约束
- 灰阶外壳红线不变:徽标图标与数字用 tokens(`--ink-*`/`--line`),不引入第三方彩色徽章
- 彩色仅正文强调(≤3 处/页)——示例文章本身要守纪律,成为正面样本而非反例
- 瞬间富文本必须复用 `renderRich` 净化链,禁止新增 set:html 直通路径
- Artalk 多实例风险:评论区折叠 + 展开挂载,若实测冲突则退回「逐条跳转到锚点页」
- 双语链路:所有新文案走 `t()`/`pickBi`,en 轨不得出现中文硬编码
- 多车道并行:content.config.ts / public/admin/config.yml 由 orchestrator 先落契约再放行,车道只改各自 boundary 内文件
@@ -0,0 +1,44 @@
# Phase 5h:内容表达能力与社交反馈 - 任务拆解
## 使用规则
- boundary/verify 齐备;verify 只读断言;车道工作树开发禁 git,收编由 orchestrator 统一提交
- 构建一律 `export PATH="$HOME/.local/node/bin:$PATH"`(Node 26)+ `pnpm astro build`
- **契约先行**:`src/content.config.ts` 与 `public/admin/config.yml` 由 orchestrator 先落契约(T1 时一并提交),车道不得再改这两个文件;需要新字段一律回报 orchestrator
- E2E 口径:`pnpm astro build → cp -a dist /tmp/dist-e2e → 用 18925 端口起快照服务 → E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs`
## 阶段一:实现
- [x] T1 文章置顶(契约 + 排序 + 标记 + 后台)
- claim: orchestrator
- boundary: src/content.config.ts(postsSchema 加 `pinned: z.boolean().optional()`,posts/postsEn 共用)、src/components/PostList.astro(置顶优先排序 + pin 标记)、src/pages/archives/index.astro(排序)、src/components/pages/{HomePage,BooksPage}.astro(排序)、public/admin/config.yml(posts 集合 pinned 开关 + 中文 hint)、src/i18n/ui.ts(置顶标签)
- verify: `E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs 2>&1 | grep -qE "汇总: [0-9]+/[0-9]+ PASS" && grep -q "pinned" src/content.config.ts && grep -q "pinned" src/components/PostList.astro && grep -q "pinned" public/admin/config.yml`
- [x] T2 瞬间富文本(字体可换 / 颜色可换 / 多形式)
- claim: worker-one
- boundary: src/components/pages/MomentsPage.astro(`text` 由纯文本 `<p>` 改为走 `renderRich` 净化+排版类管线,保留 inline 语义;空文本老数据零回归)、public/admin/config.yml 中 moments 集合 `text` 字段改 markdown widget + 提示可用排版类(**仅在 orchestrator 提交的字段块内改 widget 类型,不改结构**)、docs 提示留给 T7
- verify: `E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs 2>&1 | grep -qE "汇总: [0-9]+/[0-9]+ PASS" && grep -qE "renderRich|renderMarkdown" src/components/pages/MomentsPage.astro && python3 -c "import yaml;d=yaml.safe_load(open('public/admin/config.yml'));m=[c for c in d['collections'] if c['name']=='moments'][0];t=[f for f in m['fields'] if f['name']=='text'][0];assert t['widget']=='markdown',t"`
- [x] T3 瞬间逐条评论(Artalk 独立 page_key + 折叠懒挂载)
- claim: worker-one
- boundary: 新增 src/components/MomentComments.astro(折叠 disclosure,展开时首次挂载 Artalk;page_key 约定 `/moments/#<slug>`,en 页同一 key 不分裂;评论数徽标复用 T4 的 ViewMeta 计数接口)、src/components/pages/MomentsPage.astro(每条卡片挂评论区)
- verify: `test -f src/components/MomentComments.astro && grep -q "MomentComments" src/components/pages/MomentsPage.astro && grep -q 'moments/${slug}/' src/components/MomentComments.astro && E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs 2>&1 | grep -qE "汇总: [0-9]+/[0-9]+ PASS"`
- [x] T4 计数徽标(眼睛图标 = 浏览数,气泡图标 = 评论数)
- claim: op-artalk
- boundary: src/components/geek/ViewMeta.astro(**契约组件,由 orchestrator 在 T1 提交时一并落盘**,车道只消费不改语义)、src/components/ArtalkComments.astro(pv 小字行改为 ViewMeta,评论数取 `comments?limit=1` 的 count)、src/pages/archives/[...slug].astro、src/pages/en/archives/[...slug].astro(挂 ViewMeta)
- verify: `test -f src/components/geek/ViewMeta.astro && grep -q "ViewMeta" src/components/ArtalkComments.astro && grep -q "ViewMeta" src/pages/archives/[...slug].astro && E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs 2>&1 | grep -qE "汇总: [0-9]+/[0-9]+ PASS"`
- [x] T5 开源页重构(原生 GitHub 档案卡,去外链图依赖)
- claim: op-gitea
- boundary: 新增 src/components/geek/GithubProfile.astro(头像/名字/简介/关注者/总星标/语言占比,数据来自 projects.ts 与 GraphQL;零第三方图片与徽章)、src/lib/projects.ts(补统计聚合:followers/stars/language 汇总,失败降级不炸构建)、src/pages/opensource.astro、src/pages/en/opensource.astro(用 GithubProfile 替换 readme 渲染段;README 通道保留开关但默认 off 且标注不推荐)、content/site/opensource.yaml(注释说明 intro_from 现状)
- verify: `grep -q "GithubProfile" src/pages/opensource.astro && python3 -c "import re,pathlib;p=pathlib.Path('dist/opensource/index.html').read_text();bad=[u for u in ['img.shields.io','readme-typing-svg','github-readme-stats','streak-stats','komarev','github-readme-activity-graph'] if u in p];assert not bad,bad"`
- [x] T6 关于页承接 README 的个人叙述
- claim: op-gitea
- boundary: content/site/about.yaml(新增 blocks:关于/兴趣 prose、站点与项目 linklist、Find Me 社交块;双语 zh/en;测试值不得留在文件里)、src/pages/about.astro 与 en(仅当块渲染需要时改,优先零改动)
- verify: `python3 -c "import yaml;d=yaml.safe_load(open('content/site/about.yaml'));types=[b['type'] for b in d.get('blocks') or []];assert 'prose' in types and ('linklist' in types or 'cards' in types),types" && E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs 2>&1 | grep -qE "汇总: [0-9]+/[0-9]+ PASS"`
- [x] T7 写作模板示例文章 + 文档与 hint 收尾
- claim: orchestrator
- boundary: content/posts/writing-template.md、content/posts-en/writing-template.md(en 手写 locked:true 直译,不交给 TMT 重译)、docs/写作指南.md(模板用法 + 排版类清单去重指引)、docs/管理台.md(瞬间富文本/逐条评论/置顶/徽标说明)、public/admin/config.yml(moments text hint、posts pinned 已含)
- verify: `test -f content/posts/writing-template.md && test -f content/posts-en/writing-template.md && grep -q "pinned: true" content/posts/writing-template.md && grep -qE "font-kai|c-blue" content/posts/writing-template.md && grep -q "写作模板" docs/写作指南.md`
@@ -0,0 +1,97 @@
# Phase 5i:字体加载根治、交互缺陷修复与书架/开源页再设计 - 完成总结
## 交付结论
- 结果:完成
- 完成时间:2026-11-03 12:46
## 假设回顾
### 已验证假设
- 字体按「内容实际用到的字族」按需加载即可兼顾体积与正确性(构建期扫正文/页面文案里的 `font-*` 类)
- README 动效图均为 SVG(含 SMIL/CSS 动画),可在构建期抓取并自托管,运行时零第三方请求,动效保留
- 灯箱信息栏与关闭钮的层叠冲突用 z-index + 命中区域划分即可解,无需改结构
### 仍未完全验证的假设
- 无
## 交付范围
### 已交付
- T1 字族按需加载根治
- T2 Artalk 失败误报修复
- T3 瞬间评论区配色同步
- T4 灯箱关闭修复 + 右侧文字可排版
- T5 取消彩色强调次数限制
- T6 开源页项目墙恢复居中
- T7 书架页重做
- T8 README 动效图自托管与展示创新
- T9 字族扩展(款式待用户拍板)
### 未交付
- 无
### 偏差说明
- 无
## 简化决策
### 保持简单的关键选择
- 最简可行:字体先修门禁(用现有 6 族验证通过),再谈扩展;灯箱只改层级与命中;README 动效先接 3 类(typing / stats / snake),验证体积与稳定性后再补
- 暂不引入:字体子集化的自动重跑编排、灯箱信息栏的富文本编辑器 UI
- 不做的抽象:通用「动效资源托管」服务
### 本轮明确不做的内容
- 换 CMS、评论系统替换、Artalk 服务端改造
## 变更边界
### 本轮主要改动模块
- 见 tasks.md 的 boundary 记录
### 明确未触碰的区域
- 换 CMS、评论系统替换、Artalk 服务端改造
## 验证证据
### 备注
- 机器证据:runs.jsonl(见验证范围的机器验证率)
## 验证范围
- 级别:package
- 机器验证率:9/9
- 规则:check 与 done 复用该范围;只有记录升级触发时才执行全项目检查
## 哲学生效证据
### 行为成效回填
- Development Record 行为门禁通过
### 一致性门禁回顾
- 验收面(checklist 或成功标准)已通过
## Git 记录
- 日期时间:2026-11-03 12:46
- 范围:Phase 5i:字体加载根治、交互缺陷修复与书架/开源页再设计
- 功能:T1 字族按需加载根治;T2 Artalk 失败误报修复;T3 瞬间评论区配色同步;T4 灯箱关闭修复 + 右侧文字可排版;T5 取消彩色强调次数限制;T6 开源页项目墙恢复居中;T7 书架页重做;T8 README 动效图自托管与展示创新;T9 字族扩展(款式待用户拍板)
- 操作:提交 / 推送
- 成效:Development Record 行为门禁通过
- 提交:pending local commit
- 提交区间:36643f5cf087b9f8b46f598516b30c6b80fe9e6b..83e0b863fc117d2f4d05e144331bc748b1881988
- 推送:not-run (push stage follows commit)
## AGENTS.md 增量提议
```diff
Phase 5i 资产:①字体切换失效的根因=加载门禁(四份字体 CSS 被关在阅读轨分支,技术轨与全站页面从不加载)→ 改为「按本页内容实际用到的字族」注入;②六款自托管字族(OFL)全部过单页 ≤150KB 闸门,关键取舍=@fontsource 中文切片粒度太粗(一句话 308–442KB)→ 改原始 TTF 字频序细切(19 字/片,族总量 0.5–1.2MB);③评论失败提示误报的状态机修法(脚本标签在但库未就绪即判失败);④Artalk init 后加 .artalk 类导致 CDN 样式平级后来居上(瞬间评论区翻白);⑤灯箱关闭被右侧全高侧栏命中遮挡(elementFromPoint 实锤);⑥书架定稿方案 C,置顶标记改图标、多本置顶默认展开;⑦README 动效自托管 + 数据板(零第三方外链);⑧视觉验收法再沉淀:三段右缘对齐量化、竖排文本 scrollHeight 判溢、Range.getClientRects 判折行。
```
## 被拒绝的扩展提议
- 无
## 门禁证据
- check_spec_package.py exit 0
## 遗留事项
- 无;问题处置见下方结构化区块
## 问题处置
```json
{
"issues": [],
"version": 1
}
```
@@ -0,0 +1,18 @@
{"task": "task_0001", "cmd": "grep -q \"fontsForPage\" src/layouts/BaseLayout.astro && E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs 2>&1 | grep -qE \"汇总: [0-9]+/[0-9]+ PASS\"", "exit": 0, "duration_s": 30.681, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "ec396f2bc636b87655d31daed6960522d1c311e699ca21959bdbe363543e385b", "at": "2026-11-03T04:42:37+00:00"}
{"task": "task_0002", "cmd": "grep -q \"onerror\" src/components/ArtalkComments.astro && E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs 2>&1 | grep -qE \"汇总: [0-9]+/[0-9]+ PASS\"", "exit": 0, "duration_s": 30.242, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "ec396f2bc636b87655d31daed6960522d1c311e699ca21959bdbe363543e385b", "at": "2026-11-03T04:43:08+00:00"}
{"task": "task_0003", "cmd": "grep -qE \"data-artalk-panel|mom-atk\" src/components/MomentComments.astro && grep -q \"at-color-bg\" src/components/geek/ArtalkThemeStyles.astro && grep -q \"ArtalkThemeStyles\" src/components/ArtalkComments.astro", "exit": 0, "duration_s": 0.005, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "ec396f2bc636b87655d31daed6960522d1c311e699ca21959bdbe363543e385b", "at": "2026-11-03T04:43:08+00:00"}
{"task": "task_0004", "cmd": "grep -q \"lb-close\" src/components/pages/GalleryPage.astro && E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs 2>&1 | grep -qE \"汇总: [0-9]+/[0-9]+ PASS\"", "exit": 0, "duration_s": 30.028, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "ec396f2bc636b87655d31daed6960522d1c311e699ca21959bdbe363543e385b", "at": "2026-11-03T04:43:38+00:00"}
{"task": "task_0005", "cmd": "! grep -rq \"一页 ≤3 处\\|≤3 处/页\" docs/ content/posts/writing-template.md && grep -q \"彩色\" docs/写作指南.md", "exit": 0, "duration_s": 0.004, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "ec396f2bc636b87655d31daed6960522d1c311e699ca21959bdbe363543e385b", "at": "2026-11-03T04:43:38+00:00"}
{"task": "task_0006", "cmd": "grep -q \"align-items: center\" src/components/geek/ProjectWall.astro", "exit": 0, "duration_s": 0.003, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "ec396f2bc636b87655d31daed6960522d1c311e699ca21959bdbe363543e385b", "at": "2026-11-03T04:43:38+00:00"}
{"task": "task_0007", "cmd": "E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs 2>&1 | grep -qE \"汇总: [0-9]+/[0-9]+ PASS\"", "exit": 0, "duration_s": 30.38, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "ec396f2bc636b87655d31daed6960522d1c311e699ca21959bdbe363543e385b", "at": "2026-11-03T04:44:08+00:00"}
{"task": "task_0008", "cmd": "test -f scripts/build-perf/fetch-readme-assets.mjs && grep -q \"github-assets\" scripts/build-perf/fetch-readme-assets.mjs && python3 -c \"import pathlib;p=pathlib.Path('dist/opensource/index.html').read_text();bad=[u for u in ['img.shields.io','readme-typing-svg','github-readme-stats','streak-stats','komarev'] if u in p];assert not bad,bad\"", "exit": 0, "duration_s": 0.033, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "ec396f2bc636b87655d31daed6960522d1c311e699ca21959bdbe363543e385b", "at": "2026-11-03T04:44:08+00:00"}
{"task": "task_0009", "cmd": "grep -c \"font-\" public/admin/typography-classes.js | awk '$1>=40'", "exit": 0, "duration_s": 0.003, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "ec396f2bc636b87655d31daed6960522d1c311e699ca21959bdbe363543e385b", "at": "2026-11-03T04:44:08+00:00"}
{"task": "task_0001", "cmd": "grep -q \"fontsForPage\" src/layouts/BaseLayout.astro && E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs 2>&1 | grep -qE \"汇总: [0-9]+/[0-9]+ PASS\"", "exit": 0, "duration_s": 29.962, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "ec396f2bc636b87655d31daed6960522d1c311e699ca21959bdbe363543e385b", "at": "2026-11-03T04:45:26+00:00"}
{"task": "task_0002", "cmd": "grep -q \"onerror\" src/components/ArtalkComments.astro && E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs 2>&1 | grep -qE \"汇总: [0-9]+/[0-9]+ PASS\"", "exit": 0, "duration_s": 29.872, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "ec396f2bc636b87655d31daed6960522d1c311e699ca21959bdbe363543e385b", "at": "2026-11-03T04:45:56+00:00"}
{"task": "task_0003", "cmd": "grep -qE \"data-artalk-panel|mom-atk\" src/components/MomentComments.astro && grep -q \"at-color-bg\" src/components/geek/ArtalkThemeStyles.astro && grep -q \"ArtalkThemeStyles\" src/components/ArtalkComments.astro", "exit": 0, "duration_s": 0.005, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "ec396f2bc636b87655d31daed6960522d1c311e699ca21959bdbe363543e385b", "at": "2026-11-03T04:45:56+00:00"}
{"task": "task_0004", "cmd": "grep -q \"lb-close\" src/components/pages/GalleryPage.astro && E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs 2>&1 | grep -qE \"汇总: [0-9]+/[0-9]+ PASS\"", "exit": 0, "duration_s": 29.988, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "ec396f2bc636b87655d31daed6960522d1c311e699ca21959bdbe363543e385b", "at": "2026-11-03T04:46:26+00:00"}
{"task": "task_0005", "cmd": "! grep -rq \"一页 ≤3 处\\|≤3 处/页\" docs/ content/posts/writing-template.md && grep -q \"彩色\" docs/写作指南.md", "exit": 0, "duration_s": 0.004, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "ec396f2bc636b87655d31daed6960522d1c311e699ca21959bdbe363543e385b", "at": "2026-11-03T04:46:26+00:00"}
{"task": "task_0006", "cmd": "grep -q \"align-items: center\" src/components/geek/ProjectWall.astro", "exit": 0, "duration_s": 0.002, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "ec396f2bc636b87655d31daed6960522d1c311e699ca21959bdbe363543e385b", "at": "2026-11-03T04:46:26+00:00"}
{"task": "task_0007", "cmd": "E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs 2>&1 | grep -qE \"汇总: [0-9]+/[0-9]+ PASS\"", "exit": 0, "duration_s": 29.968, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "ec396f2bc636b87655d31daed6960522d1c311e699ca21959bdbe363543e385b", "at": "2026-11-03T04:46:56+00:00"}
{"task": "task_0008", "cmd": "test -f scripts/build-perf/fetch-readme-assets.mjs && grep -q \"github-assets\" scripts/build-perf/fetch-readme-assets.mjs && python3 -c \"import pathlib;p=pathlib.Path('dist/opensource/index.html').read_text();bad=[u for u in ['img.shields.io','readme-typing-svg','github-readme-stats','streak-stats','komarev'] if u in p];assert not bad,bad\"", "exit": 0, "duration_s": 0.032, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "ec396f2bc636b87655d31daed6960522d1c311e699ca21959bdbe363543e385b", "at": "2026-11-03T04:46:56+00:00"}
{"task": "task_0009", "cmd": "grep -c \"font-\" public/admin/typography-classes.js | awk '$1>=40'", "exit": 0, "duration_s": 0.003, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "ec396f2bc636b87655d31daed6960522d1c311e699ca21959bdbe363543e385b", "at": "2026-11-03T04:46:56+00:00"}
@@ -0,0 +1,93 @@
# Phase 5i:字体加载根治、交互缺陷修复与书架/开源页再设计 - 项目范围
<!-- spec-format: 2 -->
## 1. 问题定义
- **项目目标**:①**字体切换全部失效的根因修复**(不是字体没做,是加载门禁错了)②按需扩展更多字族 ③Artalk「加载失败」误报与瞬间评论区配色脱同步 ④图库灯箱右上关闭被遮挡点不动,且右侧文字要能换字体/颜色 ⑤取消「彩色 ≤3 处/页」限制 ⑥开源页项目墙恢复居中 ⑦书架页重做 ⑧GitHub README 的动效图与更多展示形式回归(自托管,不再外链)
- **目标用户**:站长(写得更自由、字体真的能换)与读者(交互不再有假失败、书架更耐看)
- **核心价值**:把「看起来能用」变成「真的能用」——本轮多数是用户实测踩到的静默失效,根因都在加载门禁与层级覆盖
## 2. 假设与待确认
### 2.1 已确认事实(本轮实测取证,2026-11-02)
- **字体失效根因**:`src/layouts/BaseLayout.astro:76` 把 serif/kai/kuaile/hwmc 四份字体 CSS 全部关在 `track === 'reading'` 里;技术轨与全站页面(默认 `track='tech'`)从不加载它们 → `.font-kai` 计算值虽为 `"LXGW WenKai Screen"` 但该 @font-face 不在文档字体集内(`document.fonts` 实测仅 Noto Sans/JetBrains),浏览器回退系统宋体/衬线,表现为「字体没换」
- **汇文明朝链路**:`/fonts/subset/hwmc/fonts.css` 由 public 手工切片提供(339 片 unicode-range),仅在阅读轨被 preload;技术轨整条链路不加载
- **Artalk 误报根因**:`ArtalkComments.astro:126` `if (document.getElementById('artalk-lib')) return initArtalk();` —— 软导航后脚本标签已在但 `window.Artalk` 尚未就绪,走 `initArtalk()` → `!lib` → `showRetry(true)` 永久显示失败提示;而评论随后正常渲染(用户所见「总显示失败但实际正常」)
- **瞬间评论区配色脱同步**:Artalk 主题变量块选择器是 `#artalk-container` 与 `body .atk-layer-wrap`;瞬间的容器 id 为 `mom-atk-<slug>`,不在作用域内 → 回落到 Artalk 默认色
- **灯箱关闭失灵根因**:`#lb-close` 位于 (1220,16,44×44),`document.elementFromPoint` 命中的是 `.lb-meta.lb-l2`(右侧信息栏压在按钮上),点击被信息栏吃掉 → `lb.close()` 永不触发(生产实测 `open` 保持 true)
- 书架页现状:色脊卡 + 年份分组网格;信息密度低、悬停无反馈、无简介/评分等可读线索
- README 动效资源清单(7 类):readme-typing-svg、github-readme-stats、streak-stats、activity-graph、贡献蛇(daily Action 产出的 SVG)、shields 徽章、komarev 访客
### 2.2 关键假设
- 字体按「内容实际用到的字族」按需加载即可兼顾体积与正确性(构建期扫正文/页面文案里的 `font-*` 类)
- README 动效图均为 SVG(含 SMIL/CSS 动画),可在构建期抓取并自托管,运行时零第三方请求,动效保留
- 灯箱信息栏与关闭钮的层叠冲突用 z-index + 命中区域划分即可解,无需改结构
### 2.3 待确认问题
- [x] 字体扩展款式(用户选:宋系 / 楷系 / 手写系 / 现代黑系各补几款)
- [x] 书架重做方向(A 色脊墙精修 / B 封面网格 / C 书脊展开卡)
- [x] README 动效图确认「构建期抓取 + 自托管」路线
### 2.4 可选解释与取舍
- 字体:只按需加载,不做「全站全字族常开」(那会把单页体积拉到数 MB)
- README 动效:自托管后仍保留原服务生成逻辑(GitHub Action / 第三方接口),只是把产物入仓并在构建期更新
- 彩色限制取消后仍保留「灰阶外壳」红线(导航/页脚/按钮/背景不染色),只取消正文次数限制
### 2.5 已证伪路径
- 「字族没做」判断错误:@font-face 与切片文件都在,问题在加载门禁(本轮实测推翻)
- 外链 README 图直接保留:既有 502/超时与外链依赖问题(上一阶段已判弃),改自托管
## 3. 功能范围
### 3.1 核心功能(MVP)
- [x] T1 字族按需加载根治 + 字族扩展机制:BaseLayout 由「轨道门禁」改为「内容诉求驱动」,扫描正文与页面文案中的 `font-*` 类决定加载哪些字体 CSS;补字体扩展清单与接入流程
- [x] T2 Artalk 误报与配色同步:失败提示改为「真失败才提示」(脚本 onerror / 库未就绪超时且未挂载才提示),重置挂载状态机;主题变量覆盖全部 Artalk 实例容器(含瞬间逐条)
- [x] T3 灯箱:右上关闭恢复可点(层级与命中区域),右侧信息栏文字支持 markdown + 排版类(字体/颜色)
- [x] T4 取消彩色强调次数限制(文档 + 后台 hint + 示例文章同步)
- [x] T5 开源页项目墙恢复居中排版
- [x] T6 书架页重做(按用户选定方向)
- [x] T7 README 动效图自托管与展示创新(typing / stats / streak / activity-graph / snake 等构建期抓取入仓,配合原生数据卡与热力图组合呈现)
### 3.2 扩展功能
- 字体扩展的实际接入(款式待用户定)
### 3.3 不在范围内
- 换 CMS、评论系统替换、Artalk 服务端改造
## 4. 最小实现路径
- 最简可行:字体先修门禁(用现有 6 族验证通过),再谈扩展;灯箱只改层级与命中;README 动效先接 3 类(typing / stats / snake),验证体积与稳定性后再补
- 暂不引入:字体子集化的自动重跑编排、灯箱信息栏的富文本编辑器 UI
- 不做的抽象:通用「动效资源托管」服务
## 5. 技术决策
- 技术栈:Astro 7.4.4 + @fontsource(自托管字体 CSS)+ Artalk v2 + 构建期 SVG 抓取缓存(复用 README 缓存模式)
- 证据协议:evidence: runs
- 本轮允许改动:src/layouts/BaseLayout.astro、src/styles/{global,tokens}.css、src/components/{ArtalkComments,MomentComments}.astro、src/components/geek/{ViewMeta,ProjectWall,GithubProfile}.astro、src/components/pages/{GalleryPage,BooksPage}.astro、src/pages/{gallery,books}.astro、src/lib/{projects,site-content}.ts、content/site/*.yaml、content/photos/*.json(caption 富文本)、content/.github-assets/(新缓存目录)、public/admin/*、scripts/build-perf/*、infra/deploy/build.sh、docs/*、tests/e2e.mjs
- 本轮不应触碰:nginx/ESA、构建发布链主干顺序、双语路由结构、Artalk 服务端配置
- Git integration branch:`spec/2026-11-02_blog-5i-fonts-polish`
### 5.1 验证策略
- 范围级别:package
- 变更对象:字体加载策略、Artalk 挂载状态机与主题作用域、灯箱层级与富文本、书架与开源页版式、自托管动效资源
- 快速检查:`pnpm astro build`(node26)+ `python3 scripts/build-perf/config-check.py .`
- 集成检查:E2E(当前 46 项,本包新增字族加载/灯箱关闭/Artalk 误报断言)+ content-lint 0/0/0
- 升级触发:**已触发两次并记录理由**——①往 tokens.css 补 emoji 回退链(触及全局字体栈)②BaseLayout 字体加载策略改写(触及全站首屏资源)。两次都执行了全站视觉复核(glm-5.3-flash:max,≤6 图/轮,两轮共 9 张)与生产实测。以下情况同样升级:Artalk 挂载状态机、灯箱层级、构建闸门顺序。
- 全项目检查:生产实测五条(字体真加载 `document.fonts` 命中、灯箱点关闭真关、评论失败提示不再误报、瞬间评论区配色与页面一致、开源页动效图零外链);**升级条件**——仅当改动触及 BaseLayout 字体策略或 Artalk 挂载状态机时升级为全站视觉复核,本包因两者都动,已排一轮视觉终审
## 6. 成功标准与验证方式
- [x] 技术轨文章里的 `.font-kai/.font-song/.font-huiwen/.font-kuaile` 真加载对应字体(`document.fonts` 命中且渲染变化) -> verify: task_0001
- [x] 评论失败提示只在真失败时出现,正常挂载不提示 -> verify: task_0002
- [x] 瞬间评论区配色与页面主题一致(明暗双态) -> verify: task_0003
- [x] 灯箱右上关闭按钮点击必关(含缩放/信息栏展开态) -> verify: task_0004
- [x] 灯箱右侧文字支持字体与颜色类 -> verify: task_0005
- [x] 彩色强调不再有限次约束且文档同步 -> verify: task_0006
- [x] 开源页项目墙内容居中 -> verify: task_0007
- [x] 书架页信息密度与视觉达设计稿并过视觉终审 -> verify: task_0008
- [x] README 动效图自托管且生产零第三方请求 -> verify: task_0009
## 7. 风险与约束
- 灰阶外壳红线不变,仅取消正文彩色次数限制
- 自制切片/新增字体必须守单页增量 ≤150KB 阈值,超阈值只做子集化或降级为系统回退
- README 动效自托管:抓取失败必须回落(隐藏该块,绝不炸构建),且产物必须入仓保证构建确定性
- Artalk 状态机改动需覆盖三条触发路径(IO / 锚点 / 滚底),不得回归「评论区空着」
- 多车道并行:BaseLayout、ArtalkComments、GalleryPage、BooksPage 各归单一车道,admin config 与 docs 由 orchestrator 统一改
@@ -0,0 +1,53 @@
# Phase 5i:字体加载根治、交互缺陷修复与书架/开源页再设计 - 任务拆解
## 使用规则
- boundary/verify 齐备;verify 只读断言;车道工作树开发禁 git,收编由 orchestrator 统一提交
- 构建一律 `export PATH="$HOME/.local/node/bin:$PATH"`(Node 26)+ `pnpm astro build`
- E2E 口径:`pnpm astro build → cp -a dist /tmp/dist-e2e-5i → gz-server.py 18925 /tmp/dist-e2e-5i → E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs`
## 阶段一:实现
- [x] T1 字族按需加载根治
- claim: worker-one
- boundary: src/layouts/BaseLayout.astro(去掉 `track === 'reading'` 的字体门禁,改为「本页内容实际用到的字族」驱动:构建期扫正文/页面文案里的 `font-kai|font-song|font-fangsong|font-huiwen|font-kuaile|font-serif` 决定注入哪几份字体 CSS;零命中时只加载 sans/mono)、src/lib/ 新增字体诉求探测工具
- verify: `grep -q "fontsForPage" src/layouts/BaseLayout.astro && E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs 2>&1 | grep -qE "汇总: [0-9]+/[0-9]+ PASS"`
- [x] T2 Artalk 失败误报修复
- claim: op-artalk
- boundary: src/components/ArtalkComments.astro(状态机重写:脚本 onerror 才是失败;`artalk-lib` 已存在但库未就绪时等 load 事件而非立即判失败;失败提示只在「确实未挂载且已超时」出现;`mounting` 在所有失败/成功路径复位)
- verify: `grep -q "onerror" src/components/ArtalkComments.astro && E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs 2>&1 | grep -qE "汇总: [0-9]+/[0-9]+ PASS"`
- [x] T3 瞬间评论区配色同步
- claim: op-artalk
- boundary: src/components/MomentComments.astro(Artalk 主题变量作用域覆盖瞬间容器 `[data-artalk-panel]`/`mom-atk-*`,复用文章页那套 `--at-color-*` 映射:建议抽出共享样式片段避免两份漂移)
- verify: `grep -qE "data-artalk-panel|mom-atk" src/components/MomentComments.astro && grep -q "at-color-bg" src/components/geek/ArtalkThemeStyles.astro && grep -q "ArtalkThemeStyles" src/components/ArtalkComments.astro`
- [x] T4 灯箱关闭修复 + 右侧文字可排版
- claim: orchestrator
- boundary: src/components/pages/GalleryPage.astro(关闭钮层级/命中区域;右侧信息栏不再拦截点击)、content/photos/*.json(caption 允许 markdown + 排版类)、必要时 src/lib/exif.ts 注释同步
- verify: `grep -q "lb-close" src/components/pages/GalleryPage.astro && E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs 2>&1 | grep -qE "汇总: [0-9]+/[0-9]+ PASS"`
- [x] T5 取消彩色强调次数限制
- claim: orchestrator
- boundary: docs/写作指南.md、docs/管理台.md、public/admin/config.yml(hint 里去掉「≤3 处」表述)、content/posts/writing-template.md + posts-en 同名(示例文章说明同步)
- verify: `! grep -rq "一页 ≤3 处\|≤3 处/页" docs/ content/posts/writing-template.md && grep -q "彩色" docs/写作指南.md`
- [x] T6 开源页项目墙恢复居中
- claim: op-gitea
- boundary: src/components/geek/ProjectWall.astro(`align-items: center` + `text-align: center`,与用户审美偏好一致)
- verify: `grep -q "align-items: center" src/components/geek/ProjectWall.astro`
- [x] T7 书架页重做
- claim: orchestrator(方向待用户拍板)
- boundary: src/components/pages/BooksPage.astro、可选 content/site/books.yaml 新配置、src/i18n/ui.ts
- verify: `E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs 2>&1 | grep -qE "汇总: [0-9]+/[0-9]+ PASS"`
- [x] T8 README 动效图自托管与展示创新
- claim: op-gitea
- boundary: 新增 scripts/build-perf/fetch-readme-assets.mjs(构建期抓 typing/stats/streak/activity-graph/snake 等 SVG 到 content/.github-assets/,失败回落隐藏该块)、src/components/geek/GithubProfile.astro 或新增动效展示组件、build.sh 守卫挂载、docs 说明
- verify: `test -f scripts/build-perf/fetch-readme-assets.mjs && grep -q "github-assets" scripts/build-perf/fetch-readme-assets.mjs && python3 -c "import pathlib;p=pathlib.Path('dist/opensource/index.html').read_text();bad=[u for u in ['img.shields.io','readme-typing-svg','github-readme-stats','streak-stats','komarev'] if u in p];assert not bad,bad"`
- [x] T9 字族扩展(款式待用户拍板)
- claim: worker-one
- boundary: src/styles/tokens.css、src/styles/global.css、public/admin/typography-classes.js、public/fonts/(新增字体切片)、docs/写作指南.md 字体清单
- verify: `grep -c "font-" public/admin/typography-classes.js | awk '$1>=40'`
+10 -1
View File
@@ -3,6 +3,9 @@ import { defineConfig } from 'astro/config';
import pagefind from 'astro-pagefind'; import pagefind from 'astro-pagefind';
import sitemap from '@astrojs/sitemap'; import sitemap from '@astrojs/sitemap';
import { execSync } from 'node:child_process'; import { execSync } from 'node:child_process';
import rehypeLinkCards, { linkCardsReport } from './src/lib/rehype-link-cards';
import rehypeClassWhitelist from './src/lib/rehype-class-whitelist';
import rehypeRaw from 'rehype-raw';
// 构建透明(T7):commit/时间经 vite.define 注入,页脚与 /about 引用。 // 构建透明(T7):commit/时间经 vite.define 注入,页脚与 /about 引用。
// 四重回退:CI 环境变量优先(GITEA_ACTIONS/GITHUB_SHA)→ 本地 git → dev 占位。 // 四重回退:CI 环境变量优先(GITEA_ACTIONS/GITHUB_SHA)→ 本地 git → dev 占位。
@@ -17,7 +20,13 @@ const date =
export default defineConfig({ export default defineConfig({
site: 'https://wait4x.online', site: 'https://wait4x.online',
integrations: [pagefind(), sitemap()], integrations: [pagefind(), sitemap(), linkCardsReport()],
markdown: {
// 独立成段的裸链接 → OG 预览卡(构建期抓取,失败回落普通链接;见 src/lib/rehype-link-cards.ts)
// 顺序:rehypeRaw 先把 markdown 里的 raw HTML 解析成 hast element(否则白名单看不到 span),
// 再白名单净化(剥 style/on*/非白名单类),最后链接卡片(节点由插件自建,不经净化)
rehypePlugins: [rehypeRaw, rehypeClassWhitelist, rehypeLinkCards],
},
i18n: { i18n: {
defaultLocale: 'zh', defaultLocale: 'zh',
locales: ['zh', 'en'], locales: ['zh', 'en'],
+66
View File
@@ -0,0 +1,66 @@
<div align="center">
<img src="https://readme-typing-svg.demolab.com?font=Fira+Code&weight=600&size=26&duration=3000&pause=1000&color=00D9FF&center=true&vCenter=true&random=false&width=620&lines=Hi+there+%F0%9F%91%8B+I'm+wait4x;Open+Source+Enthusiast+%E2%9D%A4%EF%B8%8F;Meteorology+%E2%9B%85" alt="Typing SVG" />
<br>
<a href="https://github.com/wait4xx?tab=followers"><img src="https://img.shields.io/github/followers/wait4xx?label=Followers&style=for-the-badge&color=00D9FF&labelColor=0D1117" /></a>
<a href="https://github.com/wait4xx?tab=stars"><img src="https://img.shields.io/github/stars/wait4xx?label=Stars&style=for-the-badge&color=00D9FF&labelColor=0D1117" /></a>
<a href="https://github.com/wait4xx"><img src="https://img.shields.io/badge/Open%20Source-%E2%9D%A4%EF%B8%8F-00D9FF?style=for-the-badge&labelColor=0D1117" /></a>
</div>
---
### 👋 About Me
- ⛅ An aspiring forecaster rooted in **meteorology**.
- 💻 Passionate about **computer science** and coding.
- 🌐 My web: **[wait4x.online](https://wait4x.online/)**
- ⚡ *"Embrace open source, love open source."*
---
### 📊 GitHub Stats
<div align="center">
<img height="170" src="https://github-readme-stats.vercel.app/api?username=wait4xx&show_icons=true&hide_border=true&bg_color=0D1117&title_color=00D9FF&icon_color=00D9FF&text_color=C9D1D9&include_all_commits=true&count_private=true&line_height=28" alt="stats" />
<img height="170" src="https://github-readme-stats.vercel.app/api/top-langs/?username=wait4xx&layout=compact&hide_border=true&bg_color=0D1117&title_color=00D9FF&text_color=C9D1D9&langs_count=8" alt="top-langs" />
</div>
<br>
<div align="center">
<img src="https://streak-stats.demolab.com?user=wait4xx&hide_border=true&background=0D1117&ring=00D9FF&fire=00D9FF&currStreakLabel=00D9FF&sideLabels=C9D1D9&dates=C9D1D9&currStreakNum=C9D1D9" alt="streak" />
</div>
---
### 📈 Activity Graph
<div align="center">
<img src="https://github-readme-activity-graph.vercel.app/graph?username=wait4xx&bg_color=0D1117&color=C9D1D9&line=00D9FF&point=00D9FF&area=true&area_color=00D9FF&hide_border=true" alt="activity-graph" />
</div>
---
### 🐍 Contribution Snake
<div align="center">
<img src="https://raw.githubusercontent.com/wait4xx/wait4xx/output/github-contribution-grid-snake.svg" alt="snake" />
</div>
> *This snake is auto-fed every day by a GitHub Action in this repo 🍎*
---
### 📫 Find Me
<div align="center">
<a href="https://github.com/wait4xx"><img src="https://img.shields.io/badge/GitHub-181717?style=for-the-badge&logo=github&logoColor=white" /></a>
<a href="mailto:X@alumni.nudt.edu.cn"><img src="https://img.shields.io/badge/Email-D14836?style=for-the-badge&logo=gmail&logoColor=white" /></a>
</div>
---
<div align="center">
<img src="https://komarev.com/ghpvc/?username=wait4xx&color=00D9FF&style=flat-square&label=%F0%9F%91%80+Visitors" />
<br><br>
<img src="https://readme-typing-svg.demolab.com?font=Fira+Code&size=14&duration=2000&pause=500&color=8B949E&center=true&vCenter=true&random=false&width=300&lines=Thanks+for+visiting!;See+you+in+the+commits+%F0%9F%8C%BF" alt="footer" />
</div>
+38
View File
@@ -0,0 +1,38 @@
{
"_readme": "链接卡片抓取缓存(构建确定性需求,入仓)。键=URL,值={title,desc,image,favicon,ts};ts 超 30 天刷新。手工改动安全:删条目即下次构建重抓。",
"https://www.cnblogs.com/Lenbrother/p/18673403": {
"title": "Debian 安装 Docker - 神奇小宝 - 博客园",
"desc": "卸载已有 Docker 如果你之前安装过 Docker Engine 之前,你需要卸载旧版本,避免冲突: for pkg in docker.io docker-doc docker-compose podman-docker containerd runc; do sudo apt-get rem",
"image": "https://assets.cnblogs.com/images/wechat-share.jpg",
"favicon": "https://assets.cnblogs.com/favicon_v3_3.ico",
"ts": 1793593758751
},
"https://wait4x.online/": {
"title": "wait4x",
"desc": "wait4x 的博客:阅读 · 技术 · 摄影",
"image": "",
"favicon": "https://wait4x.online/favicon.svg",
"ts": 1793593759184
},
"https://blog.csdn.net/qq_40605167/article/details/122888580": {
"title": "Docker版 E5续订的E5调用API续订服务:Microsoft 365 E5 Renew X_e5 docker",
"desc": "文章浏览阅读2.7w次,点赞10次,收藏43次。本文介绍了如何使用Docker轻松部署Microsoft365E5续订服务,包括提供小巧的镜像、一键部署命令及邮箱通知功能。用户需要具备Docker基础,并按照步骤修改邮箱和密码等参数。此外,还提供了不同CPU架构的支持。注意,已有部分账号出现被关停的情况。文章附带了Docker环境搭建教程和相关资源链接。",
"image": "https://i-blog.csdnimg.cn/blog_migrate/cover/dc9f6540d4d4330e797f1ebd0261f736.png",
"favicon": "https://g.csdnimg.cn/static/logo/favicon32.ico",
"ts": 1793593759739
},
"https://astro.build/": {
"title": "Astro",
"desc": "Astro builds fast content sites, powerful web applications, dynamic server APIs, and everything in-between.",
"image": "https://astro.build/og/astro.jpg",
"favicon": "https://astro.build/favicon.svg",
"ts": 1793593759816
},
"https://astro.build": {
"title": "Astro",
"desc": "Astro builds fast content sites, powerful web applications, dynamic server APIs, and everything in-between.",
"image": "https://astro.build/og/astro.jpg",
"favicon": "https://astro.build/favicon.svg",
"ts": 1793621147252
}
}
-5
View File
@@ -1,5 +0,0 @@
---
pubDate: 2026-04-29
tags: []
text: "@wait4x"
---
-5
View File
@@ -1,5 +0,0 @@
---
pubDate: 2026-04-29
tags: []
text: "@wait4x"
---
+10
View File
@@ -0,0 +1,10 @@
---
pubDate: 2026-11-02
tags: [站点更新]
text: |
瞬间现在和文章同级表达力:`text` 支持 markdown 与排版类,<span class="c-teal font-kai">这一句是青色 + 楷体</span>,也可以**加粗**。
另外每条瞬间自带评论区——展开才加载,不刷时间线就不为它付成本。
---
站点更新记录:瞬间富文本与逐条评论上线。
+5 -4
View File
@@ -1,6 +1,7 @@
{ {
"title": "防波堤暮色", "title": "防波堤暮色",
"image": "./1766156983093.jpg", "image": "./1766156983093.jpg",
"group": "landscape", "group": "landscape",
"alt": "暮色海岸与远处防波堤(T3 冒烟补写)。" "alt": "暮色海岸与远处防波堤(T3 冒烟补写)。",
"caption": "落日把海与城一齐烫成金箔,海鸟的剪影恰好掠过日轮"
} }
+3 -2
View File
@@ -3,5 +3,6 @@
"image": "./1766156992123.jpg", "image": "./1766156992123.jpg",
"group": "landscape", "group": "landscape",
"alt": "", "alt": "",
"exif": {} "exif": {},
} "caption": "书架暗处,那本宫崎骏分镜集被灯光单独打亮"
}
+3 -2
View File
@@ -3,5 +3,6 @@
"image": "./1766156992265.jpg", "image": "./1766156992265.jpg",
"group": "landscape", "group": "landscape",
"alt": "", "alt": "",
"exif": {} "exif": {},
} "caption": "一面白墙只问一个问题,笔锋落下处是想活出的人生"
}
+3 -2
View File
@@ -3,5 +3,6 @@
"image": "./1766156992559.jpg", "image": "./1766156992559.jpg",
"group": "landscape", "group": "landscape",
"alt": "", "alt": "",
"exif": {} "exif": {},
} "caption": "黑暗中独一句发光的话,像有人在夜里旋起一阵热风"
}
+3 -2
View File
@@ -3,5 +3,6 @@
"image": "./1766156992840.jpg", "image": "./1766156992840.jpg",
"group": "landscape", "group": "landscape",
"alt": "", "alt": "",
"exif": {} "exif": {},
} "caption": "一抹红墙,几笔白线,龙猫的轮廓就是吉卜力的门牌"
}
+3 -2
View File
@@ -3,5 +3,6 @@
"image": "./1766156997939.jpg", "image": "./1766156997939.jpg",
"group": "landscape", "group": "landscape",
"alt": "", "alt": "",
"exif": {} "exif": {},
} "caption": "夜游船驮着绿光驶过,东方明珠在它身后蓝得像天外来物"
}
+3 -2
View File
@@ -3,5 +3,6 @@
"image": "./1766156998072.jpg", "image": "./1766156998072.jpg",
"group": "landscape", "group": "landscape",
"alt": "", "alt": "",
"exif": {} "exif": {},
} "caption": "对岸灯火压得很低,渡轮拖着一线亮色划开墨色的江面"
}
+3 -2
View File
@@ -3,5 +3,6 @@
"image": "./1766156998205.jpg", "image": "./1766156998205.jpg",
"group": "landscape", "group": "landscape",
"alt": "", "alt": "",
"exif": {} "exif": {},
} "caption": "海关大楼的钟面亮成金色,人群在灯下攒动成剪影"
}
+3 -2
View File
@@ -3,5 +3,6 @@
"image": "./1766156998344.jpg", "image": "./1766156998344.jpg",
"group": "landscape", "group": "landscape",
"alt": "", "alt": "",
"exif": {} "exif": {},
} "caption": "旗在风里翻卷,塔身亮得像一支随时点火的火箭"
}
+3 -2
View File
@@ -3,5 +3,6 @@
"image": "./1766156998590.jpg", "image": "./1766156998590.jpg",
"group": "landscape", "group": "landscape",
"alt": "", "alt": "",
"exif": {} "exif": {},
} "caption": "大楼只占一个角落,剩下的天空都交给云慢慢写"
}
+3 -2
View File
@@ -3,5 +3,6 @@
"image": "./1766157003595.jpg", "image": "./1766157003595.jpg",
"group": "landscape", "group": "landscape",
"alt": "", "alt": "",
"exif": {} "exif": {},
} "caption": "草坡绿得发暗,牦牛散落其间,低头啃食高原的安静"
}
+3 -2
View File
@@ -3,5 +3,6 @@
"image": "./1766157003732.jpg", "image": "./1766157003732.jpg",
"group": "landscape", "group": "landscape",
"alt": "", "alt": "",
"exif": {} "exif": {},
} "caption": "灰白天色下,雪与黑岩互相渗透,山脊线细得像一笔勾勒"
}
+3 -2
View File
@@ -3,5 +3,6 @@
"image": "./1766157003865.jpg", "image": "./1766157003865.jpg",
"group": "landscape", "group": "landscape",
"alt": "", "alt": "",
"exif": {} "exif": {},
} "caption": "雾把山谷对折,只留下碎石和雪在白茫茫里彼此确认"
}
+3 -2
View File
@@ -3,5 +3,6 @@
"image": "./1766157003992.jpg", "image": "./1766157003992.jpg",
"group": "landscape", "group": "landscape",
"alt": "", "alt": "",
"exif": {} "exif": {},
} "caption": "一队人影在乱石坡上缓缓上移,雪山在他们头顶沉默俯视"
}
+3 -2
View File
@@ -3,5 +3,6 @@
"image": "./1766157004116.jpg", "image": "./1766157004116.jpg",
"group": "landscape", "group": "landscape",
"alt": "", "alt": "",
"exif": {} "exif": {},
} "caption": "雪原中央蹲着两团颜色,出发前把装备一件件理顺"
}
+3 -2
View File
@@ -3,5 +3,6 @@
"image": "./1766157012003.jpg", "image": "./1766157012003.jpg",
"group": "landscape", "group": "landscape",
"alt": "", "alt": "",
"exif": {} "exif": {},
} "caption": "太阳缩成一粒橘色的句点,悬在灰橙色的茫茫天幕上"
}
+3 -2
View File
@@ -3,5 +3,6 @@
"image": "./mmexport1771816358254.jpg", "image": "./mmexport1771816358254.jpg",
"group": "landscape", "group": "landscape",
"alt": "", "alt": "",
"exif": {} "exif": {},
} "caption": "成群海鸥同时起飞,蓝海与红屋顶都被翅膀搅动"
}
+3 -2
View File
@@ -3,5 +3,6 @@
"image": "./mmexport1771816377628.jpg", "image": "./mmexport1771816377628.jpg",
"group": "landscape", "group": "landscape",
"alt": "", "alt": "",
"exif": {} "exif": {},
} "caption": "垂钓的人立在礁石上,几根鱼竿替他守着整片海"
}
+3 -2
View File
@@ -3,5 +3,6 @@
"image": "./mmexport1771816382115.jpg", "image": "./mmexport1771816382115.jpg",
"group": "landscape", "group": "landscape",
"alt": "", "alt": "",
"exif": {} "exif": {},
} "caption": "长堤探进海里,堤上的人站成一小排,各自望着各自的远方"
}
+3 -2
View File
@@ -3,5 +3,6 @@
"image": "./mmexport1776564084994.jpg", "image": "./mmexport1776564084994.jpg",
"group": "landscape", "group": "landscape",
"alt": "", "alt": "",
"exif": {} "exif": {},
} "caption": "海鸥飞过之处,玻璃高塔与红瓦老城在同一帧里相遇"
}
+3 -2
View File
@@ -3,5 +3,6 @@
"image": "./mmexport1776564092433.jpg", "image": "./mmexport1776564092433.jpg",
"group": "landscape", "group": "landscape",
"alt": "", "alt": "",
"exif": {} "exif": {},
} "caption": "画面只留一圈亮处:老楼、松树、栏杆边与海鸥相伴的人群"
}
+55
View File
@@ -0,0 +1,55 @@
---
title: "Agensh: Scaling Organizational Intelligence to 1,024 Agents"
pubDate: 2026-11-01
category: tech
tags: [Paper Notes, AI Collaboration]
locked: true
---
Multi-agent systems compress the latency of complex tasks through concurrency, but mainstream frameworks share one ceiling: the central orchestrator. Task allocation, progress aggregation, and result adjudication all squeeze through a single context — the bigger the scale, the sooner the orchestrator dies. In October 2026, Microsoft Research open-sourced [Agensh](https://github.com/microsoft/Agensh) — a self-organized multi-agent harness **with no central orchestrator**, and the paper [*Scaling Organizational Intelligence to 1,024 Agents*](https://arxiv.org/abs/2609.26781) pushed it to a thousand and twenty-four agents.
Its answer is disarmingly simple: instead of building a smarter scheduling brain, hand the agents the coordination infrastructure of a human organization, as is.
> 📄 **[Read the full paper PDF online →](/en/papers/agensh/)** · arXiv 2609.26781 · Microsoft Research
## 1. Three pillars: an organization's coordination infrastructure
![From central orchestration to self-organization](../posts/assets/agensh/pillars.svg)
Agensh provides exactly three pieces of infrastructure; everything else self-organizes:
- **Shared workspace**: a git platform carries the organization's in-progress and integrated work — branches, PRs, and merge conflicts, twenty years of human machinery reused as-is;
- **Message interface**: a team channel plus direct messages with priority delivery — organization-wide broadcasts and urgent collision resolution each take their own lane;
- **Shared context**: a typed-entry board holding the organization's findings and work intentions.
The open-source reference implementation runs Gitea for the workspace and Mattermost for channel and DMs — a fully self-hosted "office" with no proprietary services. No orchestrator assigns tasks: workers find work themselves in the workspace and the context board, claim it, verify it, and merge it. The natural bottleneck of scaling — that ever-busier hub — simply does not exist.
## 2. The cooperation loop: every worker runs their own lap
![The multi-agent cooperation loop](../posts/assets/agensh/loop.svg)
Every worker concurrently and asynchronously runs the same loop: **gather context → claim a sub-task → take action → verify → merge progress**. Gathering context means reading the shared workspace and the board's accumulated knowledge; claiming is a CLAIM declaration, with collisions resolved over direct messages; verification is running the tests yourself — evidence talks; merging goes through PRs.
The most interesting design is the **economy** of the shared context: entries are deliberately tiny (about 100 characters, 300 for PATCH_SUMMARY), with the long version in a `detail` field fetched on demand; `board_read` serves only the latest 2,000 entries, and anything older is reachable only through boolean search (`,` = OR, `&` = AND) — recent memory stays small, the long tail is searched. The paper calls **FAIL the highest-value entry**: one documented falsified path stops the whole team from re-burning budget on the same dead end. New entries are pushed to peers as higher-priority updates at their next tool-call boundary.
This shared context is not Agensh's invention — §2.3 of the paper states plainly that "the shared context follows the core idea of DeLM". [DeLM](https://github.com/yuzhenmao/DeLM) ([arXiv:2606.10662](https://arxiv.org/abs/2606.10662)) ran decentralized multi-agent coordination through a shared context and task queue: its board is an append-only event log (items, claims, completions, FACT/FAIL notes, status) with bodies stored in a separate database beside the log, hard limits (≤40 files, ≤400 KB total), and a blocking `read --wait` that wakes when a peer posts. On a 10-task Terminal-Bench 4.0 subset, two agents coordinating through the shared context averaged +12.5 percentage points accuracy and a 1.78× speedup. Agensh scaled that thread to a thousand agents.
## 3. The numbers: does scaling actually help?
![Scaling gains](../posts/assets/agensh/scale.svg)
Two datasets answer "do more agents actually move the needle": on the five hardest ProgramBench tasks, 1 → 128 agents raised the pass rate from 19.31% to 28.78% (~49% relative); on building pandoc from scratch (6-hour budget, no Internet), 1 → 1,024 agents raised it from 33.89% to 55.06%. For problems where raw compute has hit diminishing returns, **the marginal return of organization is still there**.
Engineering readiness arrived too: v1.0 shipped on 2026-10-23, and v1.0.1 six days later supports Copilot, Codex, Claude Code, and Pi as the underlying single-agent harness — use whatever mainline model you like underneath. The open-source release is a single-node harness, and the v1.0 package ships with a reproduction guide, the paper, and a demo — from paper to runnable repository with no "coming soon" in between.
## 4. From paper to desktop: the story of one issue
For us this paper was never just on paper. After digesting it, on 2026-10-23 I filed [issue #13](https://github.com/cgarrot/pi-mesh/issues/13) on the pi-mesh repository: pi-mesh already had the message interface (`mesh_send`, rooms, priorities) and the shared workspace (git + `mesh_reserve`), but lacked the third pillar, shared context — proposing `board_write / board_read / board_grep` in exactly the Agensh/DeLM shape.
The maintainer cgarrot's reply is the most instructive part of the story: the direction was right, but **the hash-only ledger makes pi-mesh structurally incapable of being that board** — embedding persistence in the broker would turn a router into a database. The right shape was a separate package: join the room as a normal peer, own your storage, and let the mesh keep routing. Thus [pi-board](https://github.com/wait4xx/pi-board): v0.1 landed within a day — 55 tests, zero failures, a nine-step smoke run on a real broker — and when the issue finally closed, the official pointer landed on it. The memory layer of the [four-piece toolkit](/en/archives/pi-mesh-stack/) introduced in the previous post grew straight out of this paper.
## 5. Closing
Agensh's insight is not the number "1024" but its decomposition of organizational intelligence: workspace, messages, shared context — three pillars holding up self-organization, none of which needs a smarter brain. The decomposition holds at thousand-agent scale, holds for a local four-piece toolkit, and holds for a one-person shop with a few agent terminals. Scale changes; the pillars stay.
Related reading: [Spec Harness in Depth](/en/archives/spec-harness/) · [The pi-mesh Tool Family](/en/archives/pi-mesh-stack/)
+63
View File
@@ -0,0 +1,63 @@
---
title: "The pi-mesh Tool Family: A Four-Piece Toolkit for Local Multi-Agent Coordination"
pubDate: 2026-11-01
category: tech
tags: [Engineering, AI Collaboration]
locked: true
---
[pi](https://github.com/earendil-works/pi) is the coding-agent harness we use daily. With a single terminal it is a quiet assistant; with three or four terminals open at once, each running an agent over the same repository, questions pile up: who is online? Who is editing which file? Was that message actually seen? Who has the right to stop the build running at midnight?
A family of small tools grew around [pi-mesh](https://github.com/cgarrot/pi-mesh) to turn that coordination layer into infrastructure. Each owns one concern, and they interlock:
![The pi-mesh tool family](../posts/assets/pi-mesh-stack/stack.svg)
## 1. pi-mesh: the communication backbone
pi-mesh is a local bus: a broker detached from any session (NDJSON frames over a unix socket), each pi terminal a peer with an alias, organized into rooms. Messages arrive in under 50 ms; zero runtime dependencies, Node ≥ 20.
Its most admirable trait is the **honest status model**: `delivered` (written to the recipient's socket) ≠ `read` (injected into the recipient's session) ≠ `answered` (an explicit reply came back) — three states never conflated, none of them meaning "done". The companion **durable ledger is hash-only**: message bodies are never persisted; after a restart you can audit who sent what to whom and when, but never read the content. This self-restraint later became the family's design axiom: **the mesh stays a router, never a database.**
Three coordination needs live in the backbone:
- **`mesh_reserve` path reservations**: reserve before editing; other peers' `edit`/`write` tools get blocked on those paths — concurrent editing is prevented by mechanism, not by shouting.
- **`mesh_claim` task leases**: a single-writer lock with an `epoch:fence` (a broker restart invalidates old fences); commits carry `Mesh-Lease: task#leaseId#epoch:fence`, and the bundled commit-msg hook rejects stale-fence commits outright.
- **Capability routing `cap:`**: `mesh_send({ "to": "cap:deploy", ... })` routes the task to any idle peer advertising that capability instead of waiting for a busy teammate. Three message priorities: normal queues, urgent interrupts, force aborts the recipient's current turn and delivers once it settles (force is a control class and requires a reason).
Add `mesh_wait_all` for group verdicts (one call returns who answered and who is missing), `mesh_status` for peer activity (● working / ○ idle / ✕ stuck), and a `pimesh` CLI for session-less access (`pimesh attach` can adopt a dead session's identity). Multi-machine deployment is supported: one machine runs the broker, the rest connect as clients.
The honesty reaches down to exit codes: in the CLI, 3 means `expired` — a late reply is still delivered, you just stopped waiting; 5 means a stale lease, and the `commit-msg` hook rejects the commit on that basis. Even the host-specific edge of ESC purging the message queue is covered: a batch injected just as a turn aborts is re-delivered through an abort-safe channel once the turn settles (bounded retries), with a re-delivery counter visible in status.
## 2. pi-board: the shared context board
Messages are ephemeral; the ledger is hash-only. Where does "what we learned" live? [pi-board](https://github.com/wait4xx/pi-board) fills that layer. Five typed entries:
- **OBSERVED** — something seen, no commitment
- **FACT** — a verified truth
- **FAIL** — a falsified path (the highest-value kind: the whole team stops re-burning budget on it)
- **CLAIM** — an ownership note, paired with a `mesh_claim` lease (the board records; it never enforces)
- **PATCH_SUMMARY** — what a landed change did
The design is bounded and honest throughout: append-only local storage, refuse-not-evict (over-limit writes are refused, never silently evicted), short entries by default with a `detail` field unfolded only by id, boolean search (`,` = OR, `&` = AND) across the full history while `board_read` serves only the recent window; a search miss returns an honest empty, never a guess. Writes go to the room as ordinary broadcast frames; a new-FAIL digest line rides every turn boundary; the room's member / observer roles follow mesh semantics, so a read-only peer is never re-admitted as a writer after a reconnect. It joins the room as a normal peer — **all storage stays at the board's home; zero persistence in the broker**. A bundled `piboard` CLI does it in five verbs — join (member / observer) / watch / write / read / grep, one verb per action — so even a session-less terminal can join a room read-only and observe. The typology and the "small entries + on-demand detail + recent memory / full-history search" split come straight from the research literature (the lineage story is in [the next post](/en/archives/agensh/)).
## 3. pi-jobd: the background job daemon
`bash` blocks the current turn; `/reload` kills the process. [pi-jobd](https://github.com/cgarrot/pi-jobd) runs long commands in their own process group supervised by a per-user daemon: it returns in ~150 ms, and the job outlives the session. When to use it? Three criteria: the command will run longer than 60–90 seconds; you must keep working while it runs; or it must survive `/reload` and session end — plus "another agent should be able to stop it": `mesh_job_control` lets any authorized peer remotely stop/pause/resume someone else's background job.
The state machine is equally honest: `running / paused / stopped / exited / failed`, plus `lost` — when the daemon restarts and cannot prove pid ownership, it refuses to stop rather than signal an unproven process. One more clever touch: installing the package is not enough — the model must *know when* to use it. Three layers feed the model the decision rule: a prompt snippet, guidelines on all seven tools, and an on-demand skill.
## 4. pi-subagents: worker control
[pi-subagents](https://github.com/nicobailon/pi-subagents) manages in-process child agents: foreground children stream inside the conversation; background children live in a detached runner process you can check later. It works right after install with no slash commands to learn — just say "use reviewer to review this diff". It ships common roles like scout and reviewer, and supports saved workflows and parallel reviews. Its mesh interface is `mesh_run_control`: remotely stop or steer a peer's RPC worker.
## 5. One rehearsal of the four-piece suite
![The collaboration loop](../posts/assets/pi-mesh-stack/loop.svg)
## 6. Five minutes to onboard
One line each: mesh and subagents install as pi packages — `pi install npm:pi-mesh-extension`, `pi install npm:pi-subagents`; pi-board is not on npm yet — clone, `npm ci`, `npm run build` — and it works CLI-only (reading and writing the local board needs no mesh at all); pi-jobd builds from source the same way, with `pijob run / list / log / stop` working out of the box. Open two pi sessions in one directory and you have two peers visible to each other in `mesh_status` — "zero config" is not a slogan here.
Conversation over mesh, memory on the board, long work to jobd, exclusivity by lease — four packages, each doing its job, none overstepping. This blog's redesign was their proving ground: two mesh terminals (op-gitea, op-artalk) ran in parallel with the mainline, nineteen commits in one day; during the publish-chain outage, one lane reserved build.sh for emergency repair while an incoming hotfix edit was correctly blocked by the reservation — collisions are prevented by rule, not by reflex.
Together the four pieces contain no database and no central brain, yet pin down all four questions: who is online, who is doing what, what was learned, and who watches the long jobs. Small tools, hard boundaries, honest states — that philosophy is worth copying more than any individual package.
+60
View File
@@ -0,0 +1,60 @@
---
title: "Spec Harness in Depth: Packaging Long-Horizon Development into Verifiable Task Packages"
pubDate: 2026-11-01
category: tech
tags: [Engineering, AI Collaboration]
locked: true
---
What AI coding agents lack is not bandwidth for writing code — it is memory. A task that spans several sessions accumulates half-finished plans, already-falsified approaches, and acceptance criteria that get lost in chat history; restart the terminal and the context resets to zero. [Spec Harness](https://github.com/MicTx/spec-harness) targets exactly this: a task-package workflow tool for AI coding agents that persists long-horizon changes as a **recoverable, verifiable Development Record**, governed by scope, evidence, and Git gates. It provides no model and makes no product decisions for you.
Its worldview in one sentence: **when a coding task outlives one conversation, the missing tool is not a smarter model — it is a contract that everyone (including your future self) can read.**
## 1. The core abstraction: the task package
All work lives in a `.spec/specs/YYYY-MM-DD_slug/` directory with two core files: `spec.md` records scope, assumptions, and trade-offs (including a remarkable "falsified paths" section, more on that below), while `tasks.md` holds the task list. After closeout the whole package is archived under `.spec/specs/archive/` — the archive directory is the project's engineering log.
Every task must carry a two-line contract: **boundary** (which files this task may touch) and **verify** (how completion is proven). The verify is a read-only assertion that must exit 0 before the checkbox goes on. A real example from this blog's archived `2026-10-26_blog-5f` package, task T3:
![Task package anatomy](../posts/assets/spec-harness/package-anatomy.svg)
Boundary delimits "what may change"; verify delimits "how completion is proven" — both lines present, or the task stays unchecked. This is not formalism: with multiple agents running in parallel, boundary is the guardrail between lanes (in our practice, global style files are mainline-only; lanes request new variables instead of reaching in). Verify separates "looks done" from "proven done" — a verbal "passed" never counts.
The most interesting section of `spec.md` is **falsified paths**: rejected proposals go into the archive together with the reason for rejection. Our blog-5f package records three — hero banner treatment (rejected by visual adjudication), a green heatmap (broke the grayscale skeleton), and shrinking the title from 27px to 22px (a hierarchy anchor; reduce weight, not size). Whoever wants to try again reads the file first and saves a round of budget.
## 2. The stage pipeline: five core commands and the gates they keep
![Stage pipeline](../posts/assets/spec-harness/pipeline.svg)
Five core commands form one verifiable chain: **goal → scope → tasks → verification → archive → commit → push**. Gates stand between stages: `run` refuses while `spec.md` still has template placeholders; `done` requires every task to have boundary/verify, a fully green acceptance surface, and at least one non-placeholder piece of evidence (script output, test results); `push` merges only `spec/`-prefixed integration branches, never combines file changes with a commit, and forbids `--no-verify`.
A blocked task gets a `!` marker and surfaces to the user — never a silent skip. Actionable issues found during `check` must be written back into the package and fixed in the same round; "to-dos" are not deliverables.
**Interruption recovery is a designed norm** — the most direct answer to "tasks outlive conversations": re-run `/spec:run` at any moment and it resumes from the checked state in `tasks.md`, depending on no chat memory. Going further, `/spec:autoplan` runs interactive planning (clarify the goal, then framework, detail, and review passes; it stops when planning is ready — no auto-start), and `/spec:autorun` executes package by package, capped at 5 auto rounds by default (`--max-rounds` adjustable), resuming from the same step after a pause; the one-shot entry `/spec:goal` runs planning, execution, review, archiving, and the commit in a single pass.
The command surface totals 12 stage commands: the core profile installs the five core commands, and full adds the seven extensions — goal / autoplan / autorun / update / status / doctor / organize — where `doctor` self-checks and repairs the environment and `organize` audits structure. One easily-missed detail: installing only lays down files; it never auto-evaluates every task. If you want it to proactively ask "should this get a package?" before you start, wire the activation hooks once with `install_activation_hooks.py --mode on`. The criterion is delivery shape — an independent acceptance surface, actual implementers, a recoverable in-flight task; small fixes, read-only investigations, and one-off experiments just get done. It guards against "forgot to evaluate", not against deliberate under-reporting — the documentation is refreshingly candid about this.
## 3. Multi-agent orchestration: one orchestrator, bounded helpers
Spec Harness is deliberately conservative about multi-agent work: **the main session is the single orchestrator** — routing, integration, acceptance, and the `done`/`push` gates never leave the mainline. Sidecar work (exploration, building, reviewing — context-independent slices) may be delegated, but only after a machine route decision (`scripts/route_decision.py`) and a written assignment contract: goal, scope, excluded areas, output, and verification method, all five present. An optional `handoff.md` covers cross-session boundaries — a generative snapshot header plus an append-only entry log, with required sections that adapt to solo / multi-person / agent / cluster collaboration modes.
Extension slots plug other team processes in: `team-loop` for team cycles and `workflow-runner` for workflow fan-out, each with its README protocol and tests. The source repo also ships four orchestration agent definitions: orchestrator, planner, reviewer, confirmer.
## 4. Installation: one installer, a dozen-plus hosts
Spec Harness is a Python 3.9+ standard-library implementation (PolyForm Noncommercial license), installed in three steps:
```bash
git clone https://github.com/MicTx/spec-harness.git /tmp/spec-harness
cd /tmp/spec-harness
git checkout v0.22.2
bash install.sh
```
The installer supports a dozen-plus hosts: Claude Code, Codex, Gemini CLI, Grok, OpenCode, OpenClaw, Hermes, Pi, ZCode, DeepSeek Harness, Devin, Cursor… auto-detecting installed hosts when unspecified. Install profiles come in core / full: core ships the five core stage commands plus Git hooks; full adds all 12 stage commands and the slots/agents extensions. We use `INSTALL_HOSTS=pi bash install.sh` on Pi — the `/spec` commands work right after installation.
## 5. Running on a real blog
This blog is a user. The archive directory holds a chain of packages — from the bare-clone build rework and mobile layout fixes to the SEO unblocking: blog-5f packed eight tasks on three lanes (two mesh terminals plus the mainline) running in parallel, every task's verify a command-line assertion, page tasks wired to E2E — `E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs`, all 34 passing. Packages relayed one after another; nineteen green commits landed in a single day.
Tools iterate, but the idea does not expire: **write "what to do" as a contract, write "done" as evidence, archive the process as a log.** Whether the collaborators are humans or agents, those three things are the foundation of long-horizon engineering.
+125
View File
@@ -0,0 +1,125 @@
---
title: "Writing template: this post is the template"
pubDate: 2026-11-02
category: tech
tags: [writing, template]
pinned: true
locked: true
description: One post that exercises every typographic capability on the site — heading levels, colour emphasis, typefaces, quotes, code windows, tables, images, link cards, folding. Copy it to start a new post.
---
This is not a tutorial — it is a **template you can copy**. Every section below actually runs: what you see is what Markdown produces. To start a new post, copy this file, change the slug, and delete what you do not need.
It is also the live sample for **pinning**: `pinned: true` in the frontmatter lifts a post to the front of the home page, the archive and the bookshelf, while its date, URL and RSS timestamps stay untouched.
## 1. Heading levels and paragraphs
Use `##` for a section and `###` for a subsection. **Never use `#`** — the post title comes from the frontmatter `title`, and a second level-one heading confuses both screen readers and search snippets.
Leave a blank line between paragraphs. Inline you can use **bold**, *italic*, ~~strikethrough~~, `inline code`, and [ordinary links](https://astro.build).
## 2. Colour emphasis: as much as you need
Colour is **no longer capped** (the old "three per page" limit was lifted on 2026-11-02). The one rule that remains is the shell: navigation, footer, buttons and background stay greyscale — the shell is temperament, the body is expression.
<span class="c-blue">First use: state one key conclusion in blue.</span> Then carry on normally.
<span class="c-red">Second use: warn about a trap in red.</span> For example, “after changing a Markdown plugin you must clear `node_modules/.astro`, or the change silently does nothing”.
<span class="c-green">Confirm a working path in green.</span> If a whole paragraph deserves emphasis, mark the whole paragraph.
There are eight hues (blue / teal / green / yellow / orange / red / pink / purple), each with a standard and a `-soft` variant, plus four greyscale classes (`ink-muted` / `ink-warm` / `ink-cool` / `ink-faint`) for lowering tone — those do not count against the colour budget.
## 3. Typefaces: change only what you wrap
Body text is sans-serif by default. To give one sentence a different temperament, wrap it:
<span class="font-kai">This sentence is in Kai. Good for epigraphs, quoted lines in a review, or paragraphs that should be read slowly.</span>
<span class="font-huiwen">This sentence is in Huiwen Ming. Over a long paragraph it noticeably slows reading, so use it for short lines only.</span>
Six families are available (`font-kai`, `font-song`, `font-hei-sys`, `font-fangsong`, `font-huiwen`, `font-kuaile`), plus an artistic heading class `font-art-kuaile`. **Mind the weight**: switching a whole page to a family makes every visitor download a CJK font, so avoid changing the body default casually.
### A level-three heading looks like this
One step smaller than level two, bold, with tighter spacing. Use it to split a section into topics.
## 4. Quotes and lists
> Block quotes are for quoting someone. An attribution can follow as italics.
>
> — an invented attribution
Unordered list:
- First item
- Second item, with `inline code`
- Third item, nested:
- Child one
- Child two
Ordered lists suit steps:
1. Copy the template
2. Change the slug and title in the frontmatter
3. Delete the sections you do not need
## 5. Code blocks
Blocks of four lines or more get a greyscale window bar (three dots); shorter fragments do not, so small snippets do not look like screenshots.
```ts
// Sort contract: pinned first → newest first → id as a tie-breaker
export function sortPosts<T extends Post>(posts: T[]): T[] {
return [...posts].sort((a, b) => {
const pin = Number(b.data.pinned === true) - Number(a.data.pinned === true);
return pin !== 0 ? pin : b.data.pubDate.getTime() - a.data.pubDate.getTime();
});
}
```
Refer to variables inline like `posts`.
## 6. Tables
| Capability | Syntax | Note |
| --- | --- | --- |
| Colour emphasis | `<span class="c-blue">` | no limit (shell stays greyscale) |
| Typeface | `<span class="font-kai">` | a whole page adds font weight |
| Image | `![alt](./assets/<slug>/x.jpg)` | see the writing guide for size rules |
| Video | frontmatter `video:` | lives in public/media, ≤50MB |
| Link card | a bare URL on its own line | OG fetched at build time, falls back to a plain link |
## 7. Images
Reference images by relative path, stored under `content/posts/assets/<slug>/`; they expand on click and vectors stay sharp:
![Sample image: a greyscale coast](../posts/assets/media-sample/sample-photo.jpg)
## 8. Link cards
A bare URL on its own line becomes an OG preview card at build time; if the fetch fails it degrades to a plain link, never breaking the layout:
https://astro.build
## 9. Folding long content
Long appendices, full logs and big configs can fold away with zero JavaScript, using native `details`:
<details>
<summary>Open the folded example</summary>
A folded region can hold anything — paragraphs, lists, code blocks.
- Best for “read on demand” detail
- Not for conclusions (readers may never open it)
</details>
## 10. Closing after the rule
---
Three sentences are enough to close: what this covered, what the reader should take away, and where to go next. The template is not a demand to write ten sections — it is a promise that **the capabilities are at hand when you need to express something**.
Next time, copy this file and delete until only your point remains.
+54
View File
@@ -0,0 +1,54 @@
---
title: "Agensh:把组织智能扩展到 1,024 个智能体"
pubDate: 2026-11-01
category: tech
tags: [论文笔记, AI 协作]
---
多智能体系统能靠并发压缩复杂任务的时延,但主流框架有一个共同的天花板:中心编排器。任务分配、进度汇总、结果裁决全部挤过同一个上下文,规模越大,编排器先死。2026 年 10 月,微软研究院开源了 [Agensh](https://github.com/microsoft/Agensh)——一个**没有中心编排器**的自组织多智能体 harness,论文[《Scaling Organizational Intelligence to 1,024 Agents》](https://arxiv.org/abs/2609.26781)把它推到了一千零二十四个 agent 的规模。
它的答案出人意料地朴素:与其造一个更聪明的调度大脑,不如把一个人类组织的协同基建原样搬给 agent 们。
> 📄 **[在线研读全文 PDF →](/papers/agensh/)** · arXiv 2609.26781 · 微软研究院
## 一、三支柱:组织的协同基建
![从中心编排到自组织](./assets/agensh/pillars.svg)
Agensh 只给三样基础设施,其余全部自组织:
- **共享工作区**:git 平台承载组织的进行中工作与集成结果,分支、PR、合并冲突这些人类用了二十年的机制原样复用;
- **消息接口**:团队频道加私信,带优先级送达——组织级广播与紧急碰撞调解各走各的道;
- **共享上下文**:一块类型化条目板,存组织的发现与工作意图。
开源参考实现用的是 Gitea 承载工作区、Mattermost 承载频道与私信——一套完全开源自接的“办公室”,没有任何私有服务。没有编排器分配任务:worker 自己从工作区和上下文里找活、认领、验证、合并。规模化时天生的瓶颈(那个越来越忙的中枢)根本不存在。
## 二、协作环:每个 worker 各跑一圈
![多智能体协作环](./assets/agensh/loop.svg)
每个 worker 并发、异步地执行同一个协作环:**收集上下文 → 认领子任务 → 执行 → 验证 → 合并进度**。收集上下文时读共享工作区和条目板的存量知识;认领靠 CLAIM 声明,撞车了私聊解决;验证自己跑测试,证据说话;合并走 PR。
最有意思的设计在共享上下文的**经济学**:条目刻意短小(约 100 字符,PATCH_SUMMARY 放宽到 300),长版本放 `detail` 字段按需展开;`board_read` 只服务最近 2000 条,更早的历史只能靠布尔检索(`,` = OR,`&` = AND)翻出来——近期记忆保持小,长尾靠搜索。论文称 **FAIL 是价值最高的条目**:一条被证伪的路径写下来,就能止住全队在同一个死胡同里重复烧预算。新条目会作为较高优先级的更新,在同伴的下一个工具调用边界推送过去。
这套共享上下文不是 Agensh 的发明——论文 §2.3 明说“共享上下文遵循 DeLM 的核心思想”。[DeLM](https://github.com/yuzhenmao/DeLM)([arXiv:2606.10662](https://arxiv.org/abs/2606.10662))用共享上下文加任务队列跑去中心化多智能体:它的板是一张 append-only 事件日志(条目、认领、完成、FACT/FAIL 笔记、状态),正文存在日志旁的独立数据库,带硬上限(≤40 个文件、≤400KB 总量),还有一个阻塞式的 `read --wait`——同伴发帖即唤醒等待者。在 Terminal-Bench 4.0 的 10 项子集上,两个 agent 通过共享上下文协同,平均提升 12.5 个百分点准确率、1.78 倍加速。Agensh 把这条线索放大到了千级规模。
## 三、数字:扩容真的有用吗
![规模收益](./assets/agensh/scale.svg)
两组数据回答"加 agent 到底加不加得动":ProgramBench 最难 5 题,1 → 128 个 agent,通过率 19.31% → 28.78%(相对提升约 49%);从零构建 pandoc(6 小时预算、无外网),1 → 1,024 个 agent,通过率 33.89% → 55.06%。对纯堆算力已经边际递减的难题,**组织化的边际收益还在**。
工程可用性也到位了:v1.0 于 2026-10-23 发布,六天后的 v1.0.1 已支持 Copilot、Codex、Claude Code、Pi 作为底层单智能体 harness——单 agent 侧你可以用任何顺手的主力模型。开源版是单节点 harness,v1.0 发布包里就带着复现指南、论文与演示——从论文到可跑的仓库,中间没有隔着一句“敬请期待”。
## 四、从论文到桌面:一个 issue 的故事
这篇论文对我们不只在纸上。把它读透之后,2026-10-23 我在 pi-mesh 仓库提了[第 13 号 issue](https://github.com/cgarrot/pi-mesh/issues/13):pi-mesh 已有消息接口(`mesh_send`/房间/优先级)和共享工作区(git + `mesh_reserve` 预约),独缺第三支柱共享上下文——提议按 Agensh/DeLM 的形状加 `board_write / board_read / board_grep`。
维护者 cgarrot 的回复是整个故事里最值得学的一课:方向对,但**hash-only 台账让 pi-mesh 结构上当不了这块板**——把持久化塞进 broker 等于把路由器改造成数据库。正确的形状是独立包:以普通 peer 入房、自有存储、mesh 继续只做路由。于是有了 [pi-board](https://github.com/wait4xx/pi-board):一天内出 v0.1,55 个测试零失败、真实 broker 上九步冒烟,issue 最终关闭时官方指针就落在它身上。上一篇文章介绍的[四件套](/archives/pi-mesh-stack/),其记忆层就是从这篇论文长出来的。
## 五、结语
Agensh 的启示不是"1024"这个数字,而是它对"组织智能"的拆解:工作区、消息、共享上下文——三根支柱撑起自组织,任何一根都不需要一个更聪明的大脑。这个拆解在千级规模成立,在单机四件套成立,在单人加几个 agent 终端的小作坊同样成立。规模会变,支柱不变。
相关阅读:[Spec Harness 详解](/archives/spec-harness/) · [pi-mesh 全家桶详解](/archives/pi-mesh-stack/)
+45
View File
@@ -0,0 +1,45 @@
<svg xmlns="http://www.w3.org/2000/svg" width="960" height="330" viewBox="0 0 960 330" font-family="ui-sans-serif, system-ui, 'Noto Sans SC', 'PingFang SC', sans-serif">
<rect width="960" height="330" fill="#ffffff"/>
<text x="480" y="40" text-anchor="middle" font-size="19" font-weight="600" fill="#111113">多智能体协作环:每个 worker 并发、异步地各跑一圈</text>
<!-- 环上 5 步 -->
<g font-size="12.5" fill="#33343a">
<rect x="46" y="150" width="150" height="76" rx="10" fill="#f6f7f9" stroke="#d0d4da"/>
<text x="121" y="182" text-anchor="middle" font-weight="600" fill="#111113">① 收集上下文</text>
<text x="121" y="204" text-anchor="middle">读工作区 + 检索条目板</text>
<rect x="226" y="150" width="150" height="76" rx="10" fill="#f6f7f9" stroke="#d0d4da"/>
<text x="301" y="182" text-anchor="middle" font-weight="600" fill="#111113">② 认领子任务</text>
<text x="301" y="204" text-anchor="middle">声明 CLAIM,冲突私聊解</text>
<rect x="406" y="150" width="150" height="76" rx="10" fill="#f6f7f9" stroke="#d0d4da"/>
<text x="481" y="182" text-anchor="middle" font-weight="600" fill="#111113">③ 执行</text>
<text x="481" y="204" text-anchor="middle">本地改动推送分支</text>
<rect x="586" y="150" width="150" height="76" rx="10" fill="#f6f7f9" stroke="#d0d4da"/>
<text x="661" y="182" text-anchor="middle" font-weight="600" fill="#111113">④ 验证</text>
<text x="661" y="204" text-anchor="middle">跑测试,证据说话</text>
<rect x="766" y="150" width="150" height="76" rx="10" fill="#f6f7f9" stroke="#d0d4da"/>
<text x="841" y="182" text-anchor="middle" font-weight="600" fill="#111113">⑤ 合并进度</text>
<text x="841" y="204" text-anchor="middle">PR 合入共享工作区</text>
</g>
<g stroke="#6b6e78" stroke-width="1.5">
<line x1="196" y1="188" x2="224" y2="188"/><polygon points="224,188 215,183.5 215,192.5" fill="#6b6e78"/>
<line x1="376" y1="188" x2="404" y2="188"/><polygon points="404,188 395,183.5 395,192.5" fill="#6b6e78"/>
<line x1="556" y1="188" x2="584" y2="188"/><polygon points="584,188 575,183.5 575,192.5" fill="#6b6e78"/>
<line x1="736" y1="188" x2="764" y2="188"/><polygon points="764,188 755,183.5 755,192.5" fill="#6b6e78"/>
</g>
<path d="M 841 226 L 841 262 L 121 262 L 121 228" fill="none" stroke="#6b6e78" stroke-width="1.5"/>
<polygon points="121,228 116,237 126,237" fill="#6b6e78"/>
<text x="481" y="256" text-anchor="middle" font-size="12" fill="#6b6e78" dy="-6">下一圈</text>
<!-- 共享上下文条 -->
<rect x="120" y="70" width="720" height="52" rx="10" fill="rgba(52,84,209,0.05)" stroke="#3454d1" stroke-dasharray="6 4" stroke-width="1.2"/>
<text x="480" y="92" text-anchor="middle" font-size="13" font-weight="600" fill="#3454d1">共享上下文(类型化条目:OBSERVED / FACT / FAIL / CLAIM / PATCH_SUMMARY)</text>
<text x="480" y="112" text-anchor="middle" font-size="11.5" fill="#6b6e78">全程发布发现;条目刻意短小(约 100 字符),长文 detail 按需展开;近 2000 条常驻,全史靠布尔检索</text>
<line x1="200" y1="122" x2="150" y2="148" stroke="#a0a3ad" stroke-dasharray="4 3"/>
<line x1="760" y1="122" x2="820" y2="148" stroke="#a0a3ad" stroke-dasharray="4 3"/>
<text x="480" y="308" text-anchor="middle" font-size="12.5" fill="#6b6e78">论文称 FAIL 是"价值最高的条目":一条被证伪的路,能止住全队的重复预算</text>
</svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

+64
View File
@@ -0,0 +1,64 @@
<svg xmlns="http://www.w3.org/2000/svg" width="960" height="420" viewBox="0 0 960 420" font-family="ui-sans-serif, system-ui, 'Noto Sans SC', 'PingFang SC', sans-serif">
<rect width="960" height="420" fill="#ffffff"/>
<text x="480" y="38" text-anchor="middle" font-size="19" font-weight="600" fill="#111113">从中心编排到自组织:Agensh 的三支柱</text>
<!-- 左:中心编排器 -->
<rect x="40" y="70" width="360" height="290" rx="10" fill="#f6f7f9" stroke="#d0d4da"/>
<text x="220" y="98" text-anchor="middle" font-size="13.5" font-weight="600" fill="#6b6e78">传统多智能体:中心编排器瓶颈</text>
<rect x="150" y="116" width="140" height="40" rx="8" fill="#ffffff" stroke="#6b6e78"/>
<text x="220" y="141" text-anchor="middle" font-size="12.5" fill="#33343a">orchestrator</text>
<g stroke="#a0a3ad">
<line x1="220" y1="156" x2="100" y2="230"/>
<line x1="220" y1="156" x2="180" y2="230"/>
<line x1="220" y1="156" x2="260" y2="230"/>
<line x1="220" y1="156" x2="340" y2="230"/>
</g>
<g font-size="11.5" fill="#6b6e78">
<rect x="72" y="230" width="56" height="30" rx="6" fill="#ffffff" stroke="#d0d4da"/><text x="100" y="249" text-anchor="middle">w1</text>
<rect x="152" y="230" width="56" height="30" rx="6" fill="#ffffff" stroke="#d0d4da"/><text x="180" y="249" text-anchor="middle">w2</text>
<rect x="232" y="230" width="56" height="30" rx="6" fill="#ffffff" stroke="#d0d4da"/><text x="260" y="249" text-anchor="middle">w3</text>
<rect x="312" y="230" width="56" height="30" rx="6" fill="#ffffff" stroke="#d0d4da"/><text x="340" y="249" text-anchor="middle">…</text>
</g>
<text x="220" y="300" text-anchor="middle" font-size="12" fill="#6b6e78">任务分配、进度汇总、结果裁决</text>
<text x="220" y="320" text-anchor="middle" font-size="12" fill="#6b6e78">全部挤过同一个上下文——</text>
<text x="220" y="340" text-anchor="middle" font-size="12" fill="#6b6e78">规模越大,编排器先成为天花板</text>
<!-- 中间箭头 -->
<line x1="410" y1="215" x2="460" y2="215" stroke="#6b6e78" stroke-width="1.6"/>
<polygon points="460,215 450,210 450,220" fill="#6b6e78"/>
<!-- 右:自组织三支柱 -->
<rect x="470" y="70" width="450" height="290" rx="10" fill="#ffffff" stroke="#3454d1" stroke-width="1.4"/>
<text x="695" y="98" text-anchor="middle" font-size="13.5" font-weight="600" fill="#3454d1">Agensh:无中心编排,三支柱基础设施</text>
<g font-size="12" fill="#33343a">
<rect x="490" y="114" width="130" height="86" rx="8" fill="#f6f7f9" stroke="#d0d4da"/>
<text x="555" y="140" text-anchor="middle" font-weight="600" fill="#111113">共享工作区</text>
<text x="555" y="160" text-anchor="middle">git 平台</text>
<text x="555" y="178" text-anchor="middle">分支 / PR / 合并</text>
<rect x="630" y="114" width="130" height="86" rx="8" fill="#f6f7f9" stroke="#d0d4da"/>
<text x="695" y="140" text-anchor="middle" font-weight="600" fill="#111113">消息接口</text>
<text x="695" y="160" text-anchor="middle">团队频道 + 私信</text>
<text x="695" y="178" text-anchor="middle">优先级送达</text>
<rect x="770" y="114" width="130" height="86" rx="8" fill="#f6f7f9" stroke="#d0d4da"/>
<text x="835" y="140" text-anchor="middle" font-weight="600" fill="#111113">共享上下文</text>
<text x="835" y="160" text-anchor="middle">类型化条目板</text>
<text x="835" y="178" text-anchor="middle">近期记忆 + 全史检索</text>
</g>
<g stroke="#a0a3ad">
<line x1="555" y1="200" x2="520" y2="240"/>
<line x1="695" y1="200" x2="660" y2="240"/>
<line x1="835" y1="200" x2="800" y2="240"/>
<line x1="555" y1="200" x2="620" y2="240"/>
<line x1="695" y1="200" x2="740" y2="240"/>
<line x1="835" y1="200" x2="880" y2="240"/>
</g>
<g font-size="11.5" fill="#6b6e78">
<rect x="492" y="240" width="56" height="30" rx="6" fill="#ffffff" stroke="#d0d4da"/><text x="520" y="259" text-anchor="middle">w1</text>
<rect x="592" y="240" width="56" height="30" rx="6" fill="#ffffff" stroke="#d0d4da"/><text x="620" y="259" text-anchor="middle">w2</text>
<rect x="692" y="240" width="56" height="30" rx="6" fill="#ffffff" stroke="#d0d4da"/><text x="740" y="259" text-anchor="middle">w3</text>
<rect x="792" y="240" width="56" height="30" rx="6" fill="#ffffff" stroke="#d0d4da"/><text x="820" y="259" text-anchor="middle">w4</text>
<rect x="852" y="240" width="56" height="30" rx="6" fill="#ffffff" stroke="#d0d4da"/><text x="880" y="259" text-anchor="middle">…</text>
</g>
<text x="695" y="308" text-anchor="middle" font-size="12" fill="#6b6e78">worker 自己跑协作环:收集上下文 → 认领 → 执行 → 验证 → 合并</text>
<text x="695" y="330" text-anchor="middle" font-size="12" fill="#6b6e78">发现发布到共享上下文,同伴在下一个工具调用边界收到广播</text>
</svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

+34
View File
@@ -0,0 +1,34 @@
<svg xmlns="http://www.w3.org/2000/svg" width="960" height="380" viewBox="0 0 960 380" font-family="ui-sans-serif, system-ui, 'Noto Sans SC', 'PingFang SC', sans-serif">
<rect width="960" height="380" fill="#ffffff"/>
<text x="480" y="38" text-anchor="middle" font-size="19" font-weight="600" fill="#111113">规模收益:扩容 agent 带来的通过率提升</text>
<text x="480" y="58" text-anchor="middle" font-size="11.5" fill="#a0a3ad">数据:arXiv:2609.26781</text>
<!-- 组 1:ProgramBench -->
<text x="235" y="92" text-anchor="middle" font-size="13.5" font-weight="600" fill="#111113">ProgramBench 最难 5 题</text>
<text x="235" y="112" text-anchor="middle" font-size="12" fill="#6b6e78">1 → 128 agents</text>
<g>
<!-- 柱底 y=300,柱顶 = 300 - 值*3.4 -->
<rect x="115" y="235" width="100" height="65" fill="#a0a3ad"/>
<rect x="255" y="202" width="100" height="98" fill="#3454d1"/>
<text x="165" y="227" text-anchor="middle" font-size="14" font-weight="600" fill="#33343a">19.31%</text>
<text x="165" y="322" text-anchor="middle" font-size="12" fill="#6b6e78">1 agent</text>
<text x="305" y="194" text-anchor="middle" font-size="14" font-weight="600" fill="#3454d1">28.78%</text>
<text x="305" y="322" text-anchor="middle" font-size="12" fill="#6b6e78">128 agents</text>
</g>
<text x="235" y="352" text-anchor="middle" font-size="12" fill="#33343a">相对提升约 49%</text>
<!-- 组 2:pandoc -->
<text x="645" y="92" text-anchor="middle" font-size="13.5" font-weight="600" fill="#111113">从零构建 pandoc(6h 预算 · 无外网)</text>
<text x="645" y="112" text-anchor="middle" font-size="12" fill="#6b6e78">1 → 1,024 agents</text>
<g>
<rect x="525" y="187" width="100" height="113" fill="#a0a3ad"/>
<rect x="665" y="113" width="100" height="187" fill="#3454d1"/>
<text x="575" y="179" text-anchor="middle" font-size="14" font-weight="600" fill="#33343a">33.89%</text>
<text x="575" y="322" text-anchor="middle" font-size="12" fill="#6b6e78">1 agent</text>
<text x="715" y="105" text-anchor="middle" font-size="14" font-weight="600" fill="#3454d1">55.06%</text>
<text x="715" y="322" text-anchor="middle" font-size="12" fill="#6b6e78">1,024 agents</text>
</g>
<text x="645" y="352" text-anchor="middle" font-size="12" fill="#33343a">+21.2 个百分点</text>
<line x1="480" y1="84" x2="480" y2="330" stroke="#e3e5ea"/>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

@@ -0,0 +1,47 @@
<svg xmlns="http://www.w3.org/2000/svg" width="980" height="330" viewBox="0 0 980 330" font-family="ui-sans-serif, system-ui, 'Noto Sans SC', 'PingFang SC', sans-serif">
<rect width="980" height="330" fill="#ffffff"/>
<text x="490" y="40" text-anchor="middle" font-size="19" font-weight="600" fill="#111113">一次典型合奏:四件套怎么咬合</text>
<!-- 5 nodes -->
<rect x="30" y="80" width="158" height="96" rx="10" fill="#f6f7f9" stroke="#d0d4da"/>
<text x="109" y="108" text-anchor="middle" font-size="14" font-weight="600" fill="#3454d1" font-family="ui-monospace, monospace">mesh_send</text>
<text x="109" y="132" text-anchor="middle" font-size="12" fill="#33343a">契约式派单 / 对话</text>
<text x="109" y="152" text-anchor="middle" font-size="11.5" fill="#6b6e78">五字段任务包 + 期限</text>
<rect x="216" y="80" width="158" height="96" rx="10" fill="#f6f7f9" stroke="#d0d4da"/>
<text x="295" y="108" text-anchor="middle" font-size="14" font-weight="600" fill="#3454d1" font-family="ui-monospace, monospace">board_grep</text>
<text x="295" y="132" text-anchor="middle" font-size="12" fill="#33343a">先查团队记忆</text>
<text x="295" y="152" text-anchor="middle" font-size="11.5" fill="#6b6e78">FAIL 可检索不重烧</text>
<rect x="402" y="80" width="158" height="96" rx="10" fill="#f6f7f9" stroke="#d0d4da"/>
<text x="481" y="108" text-anchor="middle" font-size="14" font-weight="600" fill="#3454d1" font-family="ui-monospace, monospace">job_run</text>
<text x="481" y="132" text-anchor="middle" font-size="12" fill="#33343a">长活交后台</text>
<text x="481" y="152" text-anchor="middle" font-size="11.5" fill="#6b6e78">turn 不阻塞</text>
<rect x="588" y="80" width="158" height="96" rx="10" fill="#f6f7f9" stroke="#d0d4da"/>
<text x="667" y="108" text-anchor="middle" font-size="14" font-weight="600" fill="#3454d1" font-family="ui-monospace, monospace">mesh_claim</text>
<text x="667" y="132" text-anchor="middle" font-size="12" fill="#33343a">单写者租约</text>
<text x="667" y="152" text-anchor="middle" font-size="11.5" fill="#6b6e78">epoch:fence 围栏</text>
<rect x="774" y="80" width="158" height="96" rx="10" fill="#f6f7f9" stroke="#d0d4da"/>
<text x="853" y="108" text-anchor="middle" font-size="14" font-weight="600" fill="#3454d1" font-family="ui-monospace, monospace">board_write</text>
<text x="853" y="132" text-anchor="middle" font-size="12" fill="#33343a">留痕</text>
<text x="853" y="152" text-anchor="middle" font-size="11.5" fill="#6b6e78">CLAIM 带租约号</text>
<!-- 正向箭头 -->
<line x1="188" y1="128" x2="214" y2="128" stroke="#6b6e78" stroke-width="1.5"/>
<polygon points="214,128 205,123.5 205,132.5" fill="#6b6e78"/>
<line x1="374" y1="128" x2="400" y2="128" stroke="#6b6e78" stroke-width="1.5"/>
<polygon points="400,128 391,123.5 391,132.5" fill="#6b6e78"/>
<line x1="560" y1="128" x2="586" y2="128" stroke="#6b6e78" stroke-width="1.5"/>
<polygon points="586,128 577,123.5 577,132.5" fill="#6b6e78"/>
<line x1="746" y1="128" x2="772" y2="128" stroke="#6b6e78" stroke-width="1.5"/>
<polygon points="772,128 763,123.5 763,132.5" fill="#6b6e78"/>
<!-- 回环箭头 -->
<path d="M 853 176 L 853 216 L 109 216 L 109 178" fill="none" stroke="#a0a3ad" stroke-width="1.4" stroke-dasharray="5 4"/>
<polygon points="109,178 104,187 114,187" fill="#a0a3ad"/>
<text x="481" y="238" text-anchor="middle" font-size="12" fill="#6b6e78">发现与结论回写团队记忆,下一轮派单从存量知识起步</text>
<text x="490" y="290" text-anchor="middle" font-size="13.5" fill="#33343a">对话走 mesh · 记忆上 board · 长活交 jobd · 排他靠租约</text>
</svg>

After

Width:  |  Height:  |  Size: 3.6 KiB

@@ -0,0 +1,47 @@
<svg xmlns="http://www.w3.org/2000/svg" width="960" height="470" viewBox="0 0 960 470" font-family="ui-sans-serif, system-ui, 'Noto Sans SC', 'PingFang SC', sans-serif">
<rect width="960" height="470" fill="#ffffff"/>
<text x="480" y="38" text-anchor="middle" font-size="19" font-weight="600" fill="#111113">pi-mesh 工具族:一个路由器,三个卫星</text>
<!-- 顶部:pi 终端 -->
<rect x="80" y="62" width="200" height="58" rx="10" fill="#f6f7f9" stroke="#d0d4da"/>
<text x="180" y="86" text-anchor="middle" font-size="13.5" font-weight="600" fill="#111113">pi 终端 @op-gitea</text>
<text x="180" y="106" text-anchor="middle" font-size="11.5" fill="#6b6e78">edit/write 受预约拦截</text>
<rect x="380" y="62" width="200" height="58" rx="10" fill="#f6f7f9" stroke="#d0d4da"/>
<text x="480" y="86" text-anchor="middle" font-size="13.5" font-weight="600" fill="#111113">pi 终端 @op-artalk</text>
<text x="480" y="106" text-anchor="middle" font-size="11.5" fill="#6b6e78">读回执 · 忙闲状态</text>
<rect x="680" y="62" width="200" height="58" rx="10" fill="#f6f7f9" stroke="#d0d4da"/>
<text x="780" y="86" text-anchor="middle" font-size="13.5" font-weight="600" fill="#111113">pimesh attach / CLI</text>
<text x="780" y="106" text-anchor="middle" font-size="11.5" fill="#6b6e78">无会话独立 peer</text>
<line x1="180" y1="120" x2="180" y2="150" stroke="#a0a3ad"/>
<line x1="480" y1="120" x2="480" y2="150" stroke="#a0a3ad"/>
<line x1="780" y1="120" x2="780" y2="150" stroke="#a0a3ad"/>
<!-- mesh 骨干 -->
<rect x="60" y="150" width="840" height="110" rx="12" fill="#ffffff" stroke="#3454d1" stroke-width="1.6"/>
<text x="90" y="180" font-size="15" font-weight="600" fill="#3454d1">pi-mesh —— 通信骨干(github.com/cgarrot/pi-mesh)</text>
<text x="90" y="206" font-size="12.5" fill="#33343a">broker(内存态:peers / rooms / 邮箱 / 限速)· 房间与角色 member/observer</text>
<text x="90" y="228" font-size="12.5" fill="#33343a">mesh_reserve 路径预约 · mesh_claim 单写者租约(epoch:fence,commit-msg 钩子拒过期围栏)</text>
<text x="90" y="250" font-size="12.5" fill="#33343a">优先级 normal / urgent / force · 状态诚实三档 delivered ≠ read ≠ answered · 台账 hash-only</text>
<!-- 连接线到卫星 -->
<line x1="240" y1="260" x2="240" y2="300" stroke="#a0a3ad"/>
<line x1="480" y1="260" x2="480" y2="300" stroke="#a0a3ad"/>
<line x1="720" y1="260" x2="720" y2="300" stroke="#a0a3ad"/>
<!-- 三个卫星 -->
<rect x="70" y="300" width="330" height="110" rx="10" fill="#f6f7f9" stroke="#d0d4da"/>
<text x="90" y="328" font-size="14" font-weight="600" fill="#111113">pi-board —— 团队记忆板</text>
<text x="90" y="350" font-size="12" fill="#33343a">OBSERVED/FACT/FAIL/CLAIM/PATCH_SUMMARY</text>
<text x="90" y="370" font-size="12" fill="#33343a">有界 append-only 存储 · 布尔检索 board_grep</text>
<text x="90" y="390" font-size="11.5" fill="#6b6e78">以普通 peer 入房;自有存储,broker 零持久化</text>
<rect x="430" y="300" width="330" height="110" rx="10" fill="#f6f7f9" stroke="#d0d4da"/>
<text x="450" y="328" font-size="14" font-weight="600" fill="#111113">pi-jobd —— 后台作业守护</text>
<text x="450" y="350" font-size="12" fill="#33343a">脱离 turn 的进程组 · 存活于 /reload 与会话之外</text>
<text x="450" y="370" font-size="12" fill="#33343a">诚实状态机(lost = 无法证明归属则拒操作)</text>
<text x="450" y="390" font-size="11.5" fill="#6b6e78">mesh_job_control 跨 peer 远程 stop/pause/resume</text>
<rect x="70" y="424" width="690" height="38" rx="8" fill="rgba(52,84,209,0.05)" stroke="#3454d1" stroke-dasharray="6 4" stroke-width="1.1"/>
<text x="415" y="448" text-anchor="middle" font-size="12" fill="#33343a">pi-subagents(worker 控制:spawn/steer/abort,mesh_run_control)与上面三者同族协作</text>
</svg>

After

Width:  |  Height:  |  Size: 3.9 KiB

@@ -0,0 +1,37 @@
<svg xmlns="http://www.w3.org/2000/svg" width="960" height="440" viewBox="0 0 960 440" font-family="ui-sans-serif, system-ui, 'Noto Sans SC', 'PingFang SC', sans-serif">
<rect width="960" height="440" fill="#ffffff"/>
<text x="480" y="40" text-anchor="middle" font-size="19" font-weight="600" fill="#111113">任务包解剖:.spec/specs/YYYY-MM-DD_slug/</text>
<!-- 左:目录树 -->
<rect x="40" y="70" width="400" height="250" rx="10" fill="#f6f7f9" stroke="#d0d4da"/>
<text x="60" y="102" font-size="15" font-weight="600" fill="#111113" font-family="ui-monospace, monospace">2026-10-26_blog-5f/</text>
<text x="80" y="140" font-size="14" fill="#33343a" font-family="ui-monospace, monospace">├── spec.md</text>
<text x="240" y="140" font-size="13" fill="#6b6e78">范围 · 假设 · 已证伪</text>
<text x="80" y="180" font-size="14" fill="#33343a" font-family="ui-monospace, monospace">├── tasks.md</text>
<text x="240" y="180" font-size="13" fill="#6b6e78">boundary + verify</text>
<text x="80" y="220" font-size="14" fill="#33343a" font-family="ui-monospace, monospace">└── handoff.md</text>
<text x="240" y="220" font-size="13" fill="#6b6e78">跨会话交接(可选)</text>
<line x1="80" y1="248" x2="400" y2="248" stroke="#e3e5ea"/>
<text x="80" y="278" font-size="13" fill="#6b6e78">收尾后整包归档至 .spec/specs/archive/,</text>
<text x="80" y="300" font-size="13" fill="#6b6e78">归档目录就是项目的工程日志。</text>
<!-- 箭头 -->
<line x1="440" y1="195" x2="500" y2="195" stroke="#6b6e78" stroke-width="1.5"/>
<polygon points="500,195 490,190 490,200" fill="#6b6e78"/>
<text x="470" y="180" text-anchor="middle" font-size="12" fill="#6b6e78">每个任务</text>
<!-- 右:任务卡片 -->
<rect x="500" y="70" width="420" height="250" rx="10" fill="#ffffff" stroke="#3454d1" stroke-width="1.5"/>
<rect x="500" y="70" width="420" height="40" rx="10" fill="rgba(52,84,209,0.06)"/>
<text x="520" y="96" font-size="13.5" font-weight="600" fill="#111113">- [x] T3 构建期内容 lint + build.sh 守卫</text>
<text x="520" y="136" font-size="12.5" fill="#33343a" font-family="ui-monospace, monospace">claim: op-gitea(mesh 车道)</text>
<text x="520" y="164" font-size="12" fill="#33343a" font-family="ui-monospace, monospace">boundary: 新建 content-lint.mjs + 守卫挂载</text>
<text x="520" y="200" font-size="12.5" font-weight="600" fill="#3454d1" font-family="ui-monospace, monospace">verify:</text>
<text x="540" y="224" font-size="12" fill="#33343a" font-family="ui-monospace, monospace">node scripts/build-perf/content-lint.mjs dist</text>
<text x="540" y="246" font-size="12" fill="#33343a" font-family="ui-monospace, monospace">&amp;&amp; bash -n infra/deploy/build.sh</text>
<text x="520" y="290" font-size="12.5" fill="#6b6e78">verify 是只读断言:跑出退出码 0 才许勾选,</text>
<text x="520" y="310" font-size="12.5" fill="#6b6e78">口头"过了"永远不算数。</text>
<text x="480" y="372" text-anchor="middle" font-size="13.5" fill="#6b6e78">boundary 划定"能动什么",verify 划定"怎么证明做完"——两行合同,缺一不勾</text>
<line x1="360" y1="392" x2="600" y2="392" stroke="#e3e5ea"/>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

@@ -0,0 +1,51 @@
<svg xmlns="http://www.w3.org/2000/svg" width="960" height="350" viewBox="0 0 960 350" font-family="ui-sans-serif, system-ui, 'Noto Sans SC', 'PingFang SC', sans-serif">
<rect width="960" height="350" fill="#ffffff"/>
<text x="480" y="38" text-anchor="middle" font-size="19" font-weight="600" fill="#111113">阶段流水线:五个核心命令与它们把守的门</text>
<!-- 5 个节点 -->
<!-- new -->
<rect x="36" y="72" width="152" height="120" rx="10" fill="#f6f7f9" stroke="#d0d4da"/>
<text x="112" y="100" text-anchor="middle" font-size="14" font-weight="600" fill="#3454d1" font-family="ui-monospace, monospace">/spec:new</text>
<text x="112" y="126" text-anchor="middle" font-size="12.5" fill="#33343a">建任务包</text>
<text x="112" y="146" text-anchor="middle" font-size="12.5" fill="#33343a">spec.md + tasks.md</text>
<text x="112" y="172" text-anchor="middle" font-size="11.5" fill="#6b6e78">模板占位未填 → 拒 run</text>
<!-- run -->
<rect x="220" y="72" width="152" height="120" rx="10" fill="#f6f7f9" stroke="#d0d4da"/>
<text x="296" y="100" text-anchor="middle" font-size="14" font-weight="600" fill="#3454d1" font-family="ui-monospace, monospace">/spec:run</text>
<text x="296" y="126" text-anchor="middle" font-size="12.5" fill="#33343a">逐任务执行</text>
<text x="296" y="146" text-anchor="middle" font-size="12.5" fill="#33343a">verify 跑通才勾选</text>
<text x="296" y="172" text-anchor="middle" font-size="11.5" fill="#6b6e78">阻塞任务打 ! 上浮</text>
<!-- check -->
<rect x="404" y="72" width="152" height="120" rx="10" fill="#f6f7f9" stroke="#d0d4da"/>
<text x="480" y="100" text-anchor="middle" font-size="14" font-weight="600" fill="#3454d1" font-family="ui-monospace, monospace">/spec:check</text>
<text x="480" y="126" text-anchor="middle" font-size="12.5" fill="#33343a">复核轮</text>
<text x="480" y="146" text-anchor="middle" font-size="12.5" fill="#33343a">问题当轮回写修复</text>
<text x="480" y="172" text-anchor="middle" font-size="11.5" fill="#6b6e78">闭环项五选一登记</text>
<!-- done -->
<rect x="588" y="72" width="152" height="120" rx="10" fill="#f6f7f9" stroke="#d0d4da"/>
<text x="664" y="100" text-anchor="middle" font-size="14" font-weight="600" fill="#3454d1" font-family="ui-monospace, monospace">/spec:done</text>
<text x="664" y="126" text-anchor="middle" font-size="12.5" fill="#33343a">归档 + 包提交</text>
<text x="664" y="146" text-anchor="middle" font-size="12.5" fill="#33343a">证据随包入档</text>
<text x="664" y="172" text-anchor="middle" font-size="11.5" fill="#6b6e78">验收面证据必须真实</text>
<!-- push -->
<rect x="772" y="72" width="152" height="120" rx="10" fill="#f6f7f9" stroke="#d0d4da"/>
<text x="848" y="100" text-anchor="middle" font-size="14" font-weight="600" fill="#3454d1" font-family="ui-monospace, monospace">/spec:push</text>
<text x="848" y="126" text-anchor="middle" font-size="12.5" fill="#33343a">合并 + 推送</text>
<text x="848" y="146" text-anchor="middle" font-size="12.5" fill="#33343a">删已并分支</text>
<text x="848" y="172" text-anchor="middle" font-size="11.5" fill="#6b6e78">只认 spec/ 前缀分支</text>
<!-- 箭头 -->
<line x1="188" y1="132" x2="218" y2="132" stroke="#6b6e78" stroke-width="1.5"/>
<polygon points="218,132 209,127.5 209,136.5" fill="#6b6e78"/>
<line x1="372" y1="132" x2="402" y2="132" stroke="#6b6e78" stroke-width="1.5"/>
<polygon points="402,132 393,127.5 393,136.5" fill="#6b6e78"/>
<line x1="556" y1="132" x2="586" y2="132" stroke="#6b6e78" stroke-width="1.5"/>
<polygon points="586,132 577,127.5 577,136.5" fill="#6b6e78"/>
<line x1="740" y1="132" x2="770" y2="132" stroke="#6b6e78" stroke-width="1.5"/>
<polygon points="770,132 761,127.5 761,136.5" fill="#6b6e78"/>
<!-- 恢复车道 -->
<rect x="36" y="228" width="888" height="76" rx="10" fill="rgba(52,84,209,0.05)" stroke="#3454d1" stroke-dasharray="6 4" stroke-width="1.2"/>
<text x="480" y="258" text-anchor="middle" font-size="13.5" font-weight="600" fill="#111113">中断恢复是常态设计</text>
<text x="480" y="284" text-anchor="middle" font-size="12.5" fill="#33343a">任意时刻重跑 /spec:run,从 tasks.md 勾选状态续跑;/spec:autorun 可续自动链(默认 5 轮上限,--max-rounds 可调)</text>
</svg>

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

+34
View File
@@ -0,0 +1,34 @@
---
title: "排版能力样例"
pubDate: 2026-11-02
category: tech
tags: [test, hidden]
# 排版/斜体/白名单测试样张(fix3 车道):hidden 不列出,构建与 E2E 验证用
draft: false
---
本篇验证三件事:斜体行内渲染、受限排版类白名单、图注类。
## 斜体(应为行内,不换行不居中)
这一句里有 *行内斜体强调* 夹在文字中间,它必须与正文同处一行。
*这一整段是斜体独占段,它不该变成块级居中。*
## 受限排版类(白名单)
<span class="ink-muted">这一段是 ink-muted 灰字。</span>
<span class="font-kai">这一段是霞鹜文楷字族。</span>
<span class="font-mono">这一段是等宽字族 mono。</span>
<p class="drop-cap">首字下沉段落:秋天的海面像一块被风抚平的铁皮,落日把它的边缘烧红了一圈。</p>
<p class="quote-line">所谓暮色,就是光慢慢地承认自己要走。</p>
## 白名单剔除(非法类与内联样式必须被剥离)
<span class="evil-class ink-2" style="color:#ff0000" onclick="alert(1)">这一段同时带了非法类、内联颜色与 onclick,构建后应只剩 ink-2 且 style/onclick 消失。</span>
## 图注(类驱动)
<p class="fig-cap">这是显式图注样式(fig-cap),居中灰字。</p>
+33
View File
@@ -0,0 +1,33 @@
---
title: "字体族样例"
pubDate: 2026-11-02
category: reading
tags: [test, hidden]
draft: false
---
字体测试样张(hidden):验证系统回退字族的渲染。
<p class="font-song">宋体族(系统回退):秋天的海面像一块被风抚平的铁皮。</p>
<p class="font-kai-sys">楷体族(系统回退):秋天的海面像一块被风抚平的铁皮。</p>
<p class="font-hei-sys">黑体族(系统回退):秋天的海面像一块被风抚平的铁皮。</p>
<p class="font-fangsong">仿宋族(系统回退):秋天的海面像一块被风抚平的铁皮。</p>
<p class="ink-warm">暖调正文:秋天的海面像一块被风抚平的铁皮。</p>
<p class="ink-cool">冷调正文:秋天的海面像一块被风抚平的铁皮。</p>
<p class="ink-faint">最淡可读档:秋天的海面像一块被风抚平的铁皮。</p>
<p class="ink-accent-soft">弱强调:秋天的海面像一块被风抚平的铁皮。</p>
<p class="c-blue">蓝色标准:秋天的海面像一块被风抚平的铁皮。</p>
<p class="c-yellow">黄色标准(已压暗):秋天的海面像一块被风抚平的铁皮。</p>
<p class="c-red-soft">红色 soft:秋天的海面像一块被风抚平的铁皮。</p>
<p class="font-huiwen">汇文明朝体(OFL,切片自托管):秋天的海面像一块被风抚平的铁皮。</p>
<p class="font-kuaile">站酷快乐体(OFL,艺术字):秋天的海面像一块被风抚平的铁皮。</p>
@@ -1,11 +1,15 @@
--- ---
title: "海风中失落的血色馈赠" title: 海风中失落的血色馈赠
pubDate: 2026-03-25 pubDate: 2026-03-25
category: reading category: reading
tags: [read] tags:
- read
mood: '#0f766e' mood: '#0f766e'
bookAuthor: '阿利斯泰尔·麦克劳德' bookAuthor: 阿利斯泰尔·麦克劳德
description: '秋是萧瑟之感:父亲、老马与少年,在失落后各自直面现实。' description: 秋是萧瑟之感:父亲、老马与少年,在失落后各自直面现实。
cover: ./assets/海风中失落的血色馈赠.png
hiddenTag: false
draft: false
--- ---
### 秋-斯科特之秋、父亲之秋、少年之秋(冬的预演) ### 秋-斯科特之秋、父亲之秋、少年之秋(冬的预演)
+30
View File
@@ -0,0 +1,30 @@
---
title: "链接卡片样例"
pubDate: 2026-11-02
category: tech
tags: [test, hidden]
# 链接卡片(OG 预览)测试样张:hidden 不列出,构建验证用;保留为活样例
draft: false
---
本篇验证链接卡片:独立成段的裸链接转 OG 预览卡,内联链接保持普通样式。
## 正例:有 OG 的站点
https://wait4x.online/
https://astro.build/
## 反例:404 与无 OG 站点(应回落普通链接,构建不报错)
https://wait4x.online/definitely-not-a-real-page-404/
https://example.com/
## 内联链接保持普通样式(不应变卡)
这里有一个内联链接 [Astro 官网](https://astro.build/) 夹在句子中间,它必须保持普通链接样式,不能整篇变卡片墙。
## 卡片首行文字(markdown 链接形态同样触发)
[wait4x.online 首页](https://wait4x.online/)
+1 -1
View File
@@ -14,7 +14,7 @@ draft: false
![样例照片](./assets/media-sample/sample-photo.jpg) ![样例照片](./assets/media-sample/sample-photo.jpg)
*图注写法:斜体单独成段,photos 目录样片副本(1600×2400 竖幅)。* <p class="fig-cap">图注新写法:fig-cap 类,photos 目录样片副本(1600×2400 竖幅)。</p>
## 视频(raw HTML,H.264 320×240,ffmpeg testsrc2 生成) ## 视频(raw HTML,H.264 320×240,ffmpeg testsrc2 生成)
+13 -31
View File
@@ -1,20 +1,25 @@
--- ---
title: "面纱" title: 面纱
pubDate: 2026-03-09 pubDate: 2026-03-09
category: reading category: reading
tags: [read] tags:
- read
pinned: true
mood: '#7c3aed' mood: '#7c3aed'
bookAuthor: '威廉·萨默塞特·毛姆' bookAuthor: 威廉·萨默塞特·毛姆
description: '面纱之下:理想的殉道者与真实的幸存者。' description: 面纱之下:理想的殉道者与真实的幸存者。
cover: ./assets/面纱.png
hiddenTag: false
draft: false
--- ---
****面纱之下:理想的殉道者和真实的幸存者**** **_面纱之下:理想的殉道者和真实的幸存者_**
### 你用面纱遮蔽他人的窥探,自己也难以一睹世界之实。 ### 你用面纱遮蔽他人的窥探,自己也难以一睹世界之实。
这是译者在后记中写下的一句话。很好印证了《面纱》这本书或者说这个题目所蕴含的真相。人们活在面纱之下,以此遮蔽他人的窥探,却也因此困于自己的面纱之下。 这是译者在后记中写下的一句话。很好印证了《面纱》这本书或者说这个题目所蕴含的真相。人们活在面纱之下,以此遮蔽他人的窥探,却也因此困于自己的面纱之下。
见沃尔特像是见最温柔的爱和最悲痛的爱情,以最炽热的心和最真挚的感情去爱,爱上了一个不爱自己的人,最后在爱中将自己陷入漩涡🌀,不得解脱。他富有智慧,又富有对这个世界和社会以及人性最明锐的洞察力,他了解世界的苦难,了解社会的种种表象以及深层次的真相,了解人的“伪装”背后的真实人性。他像是个圣洁的存在,却同样又有着拙劣和“恶毒”的想法,以至于至死都未能从感情漩涡中游出。在我看来,沃尔特对于凯蒂的爱是真切的,对于背叛的恨也是真切的,但从始至终爱始终贯穿永远。在去湄潭府之前,他是冷静的旁观者,他是理性的化身,他有真对于真理的精准见解,有着自己衡量一切的道德标准和不可被践踏的信仰,这是源于理性的不可被磨灭的印记。在感知到背叛,并带着赴死的姿态奔赴湄潭府的过程中,他被感情所裹挟,他既爱又恨,恨凯蒂的背叛,也恨自己爱上了一个与自己原则背道而驰的爱人,他从一开始想让凯蒂也走向死亡,到最后让她离开,看似对于婚姻和这段感情有了一点点解脱,可实际上到死他都未能跨过自己给自己筑起的情感高墙。他是矛盾的,他至死未能宽恕自己,也正如他对世界最后的留言所述:“死的是一条狗”。这不仅是对凯蒂的隐喻,更是他对自己灵魂的终极审判。他终于承认,那个满怀怨恨、试图以爱之名行毁灭之实的自己,才是那条被仇恨吞噬的‘疯狗’。沃尔特的一生,是用最锋利的理性去剖析世界,却唯独不敢直视自己内心那片荒芜的废墟。他死于霍乱,实则死于那层由傲慢与偏见织就的、永远无法揭下的面纱。 见沃尔特像是见最温柔的爱和最悲痛的爱情,以最炽热的心和最真挚的感情去爱,爱上了一个不爱自己的人,最后在爱中将自己陷入漩涡🌀,不得解脱。他富有智慧,又富有对这个世界和社会以及人性最明锐的洞察力,他了解世界的苦难,了解社会的种种表象以及深层次的真相,了解人的“伪装”背后的真实人性。他像是个圣洁的存在,却同样又有着拙劣和“恶毒”的想法,以至于至死都未能从感情漩涡中游出。在我看来,沃尔特对于凯蒂的爱是真切的,对于背叛的恨也是真切的,但从始至终爱始终贯穿永远。在去湄潭府之前,他是冷静的旁观者,他是理性的化身,他有着对于真理的精准见解,有着自己衡量一切的道德标准和不可被践踏的信仰,这是源于理性的不可被磨灭的印记。在感知到背叛,并带着赴死的姿态奔赴湄潭府的过程中,他被感情所裹挟,他既爱又恨,恨凯蒂的背叛,也恨自己爱上了一个与自己原则背道而驰的爱人,他从一开始想让凯蒂也走向死亡,到最后让她离开,看似对于婚姻和这段感情有了一点点解脱,可实际上到死他都未能跨过自己给自己筑起的情感高墙。他是矛盾的,他至死未能宽恕自己,也正如他对世界最后的留言所述:“死的是一条狗”。这不仅是对凯蒂的隐喻,更是他对自己灵魂的终极审判。他终于承认,那个满怀怨恨、试图以爱之名行毁灭之实的自己,才是那条被仇恨吞噬的‘疯狗’。沃尔特的一生,是用最锋利的理性去剖析世界,却唯独不敢直视自己内心那片荒芜的废墟。他死于霍乱,实则死于那层由傲慢与偏见织就的、永远无法揭下的面纱。
相比之下,凯蒂是幸运的,也最终得到了自由。在虚荣的面纱下,遮住的是世界的残酷和社会的复杂以及人性的不可知。一开始的虚荣心作祟,让她步入了和沃尔特的婚姻,却发现婚后的生活并非是自己所期望的那样,甚至从一开始她就清楚的知道自己并不爱沃尔特。于是,当她到了香港,并遇上查理的时候,她沦陷了,她在查理身上看见了自己所期待的伴侣所拥有的那些特性,幽默风趣,性感迷人,但是却忽视了人性。在去往湄潭府之前,她虚荣,她害怕,她怕沃尔特发现一切,她软弱,她想逃离这样的婚姻和查理相拥,但是她不敢付诸行动。当沃尔特推着她去面对一切的时候,当一切都败露的时候,她终究是看见了人性的恶,她见到了查理的无情和所谓的爱只是言语上的“美好”,她也真正的感到了恐惧,面对死亡的恐惧和面对沃尔特不可捉摸的想法的恐惧。在湄潭府之行的初期,她依旧活在对于过去的回忆之中,她害怕恐惧,她看不见未来。但被圣光照耀的庙堂始终矗立在彼岸,她向往那迷人的色彩,她想要去追求新的生活。当她跨出第一步并开始在修道院工作的时候,她的觉醒之路从此慢慢开始,她慢慢和自己和解,慢慢看淡背叛,慢慢看清查理,慢慢找寻着自己在生活中该有的样子,她开始慢慢摘掉自己的面纱,看清自己的虚伪和软弱,开始追寻道的意义,她感悟周围的一切,甚至想着能够将沃尔特从他的痛苦中将他拉出。然而直到沃尔特死去,他们都没能和解,她愧对沃尔特,她看到了自己的轻贱,可是她自始至终都跨不过沃尔特竖起的高墙,她在墙外,沃尔特在墙内,各自在各自的面纱下痛苦的活着。沃尔特的死,是沃尔特的一种“解脱”,也是凯蒂的一种解脱。 相比之下,凯蒂是幸运的,也最终得到了自由。在虚荣的面纱下,遮住的是世界的残酷和社会的复杂以及人性的不可知。一开始的虚荣心作祟,让她步入了和沃尔特的婚姻,却发现婚后的生活并非是自己所期望的那样,甚至从一开始她就清楚的知道自己并不爱沃尔特。于是,当她到了香港,并遇上查理的时候,她沦陷了,她在查理身上看见了自己所期待的伴侣所拥有的那些特性,幽默风趣,性感迷人,但是却忽视了人性。在去往湄潭府之前,她虚荣,她害怕,她怕沃尔特发现一切,她软弱,她想逃离这样的婚姻和查理相拥,但是她不敢付诸行动。当沃尔特推着她去面对一切的时候,当一切都败露的时候,她终究是看见了人性的恶,她见到了查理的无情和所谓的爱只是言语上的“美好”,她也真正的感到了恐惧,面对死亡的恐惧和面对沃尔特不可捉摸的想法的恐惧。在湄潭府之行的初期,她依旧活在对于过去的回忆之中,她害怕恐惧,她看不见未来。但被圣光照耀的庙堂始终矗立在彼岸,她向往那迷人的色彩,她想要去追求新的生活。当她跨出第一步并开始在修道院工作的时候,她的觉醒之路从此慢慢开始,她慢慢和自己和解,慢慢看淡背叛,慢慢看清查理,慢慢找寻着自己在生活中该有的样子,她开始慢慢摘掉自己的面纱,看清自己的虚伪和软弱,开始追寻道的意义,她感悟周围的一切,甚至想着能够将沃尔特从他的痛苦中将他拉出。然而直到沃尔特死去,他们都没能和解,她愧对沃尔特,她看到了自己的轻贱,可是她自始至终都跨不过沃尔特竖起的高墙,她在墙外,沃尔特在墙内,各自在各自的面纱下痛苦的活着。沃尔特的死,是沃尔特的一种“解脱”,也是凯蒂的一种解脱。
@@ -22,7 +27,7 @@ description: '面纱之下:理想的殉道者与真实的幸存者。'
> >
> 所谓《面纱》,遮住的不仅是真相,更是我们面对真实自我的勇气。沃尔特缺乏这份勇气去接纳不完美的爱与人性,所以他困死其中;凯蒂在生死关头被迫拥有了这份勇气,所以她获得了自由。 > 所谓《面纱》,遮住的不仅是真相,更是我们面对真实自我的勇气。沃尔特缺乏这份勇气去接纳不完美的爱与人性,所以他困死其中;凯蒂在生死关头被迫拥有了这份勇气,所以她获得了自由。
> >
> 故事的最后,死去的人得到了安息,活着的人带着记忆与教训,继续前行。这或许就是毛姆想告诉我们的:****人生没有完美的结局,只有在认清生活的残酷真相后,依然选择热爱并真实地活下去,才是唯一的救赎。**** > 故事的最后,死去的人得到了安息,活着的人带着记忆与教训,继续前行。这或许就是毛姆想告诉我们的:**_人生没有完美的结局,只有在认清生活的残酷真相后,依然选择热爱并真实地活下去,才是唯一的救赎。_**
从背叛到自我的救赎,凯蒂在远离家乡的异国,没能逃脱欲望作祟,再次回到香港之后又一次沦陷在了查理的怀抱中,但此刻的她早已不是去湄潭府的之前的自己,她的思想早已骤变。她厌恶自己肉体对自己灵魂的出卖,她选择远离这个是非之地,她选择去重新追求自由,亦如从湄潭府离开一样,她再一次走上了追寻自由的路上。她回到故土,见母亲离世,见妹妹怀有身孕,见父亲终于脱离过去的自己,她亦想脱离过去的自己,拥抱一个崭新的未来。活在面纱之下的她和她的父亲,在故事的结局,两人袒露心声,一同前往新的地方开始新的生活,去活出新的自己。 从背叛到自我的救赎,凯蒂在远离家乡的异国,没能逃脱欲望作祟,再次回到香港之后又一次沦陷在了查理的怀抱中,但此刻的她早已不是去湄潭府的之前的自己,她的思想早已骤变。她厌恶自己肉体对自己灵魂的出卖,她选择远离这个是非之地,她选择去重新追求自由,亦如从湄潭府离开一样,她再一次走上了追寻自由的路上。她回到故土,见母亲离世,见妹妹怀有身孕,见父亲终于脱离过去的自己,她亦想脱离过去的自己,拥抱一个崭新的未来。活在面纱之下的她和她的父亲,在故事的结局,两人袒露心声,一同前往新的地方开始新的生活,去活出新的自己。
@@ -32,7 +37,7 @@ description: '面纱之下:理想的殉道者与真实的幸存者。'
沃尔特死在了他的‘正确’里,而凯蒂活在了她的‘真实’中。沃尔特的故事是带有悲惨的个人主义的牺牲,凯蒂的故事是带有幸运的个人主义的新生。沃尔特自觉没有希望,在困顿的泥潭中深陷,他没有勇气去面对新的生活和新的世界,他将自己紧紧隐藏在自己的面纱之后,他活在自己为自己构建的信念世界之中,为自己的信念,"悲壮”赴死。凯蒂活在纷繁的社会中,从小我见大我,从假我见真我,从困局中得自由。她一步一步从虚伪的面纱下得见人性、社会冷暖以及新世界的希望,湄潭府之前的生活让她自省,湄潭府之行让她感知到一切,在死亡和黑暗中得到感悟和觉醒,得见光明,再次回到香港,欲望再次作祟,这一次她没有沉沦于虚妄的现实,她终于携手勇气做出了自己的决定,踏上了"离开”的归程。 沃尔特死在了他的‘正确’里,而凯蒂活在了她的‘真实’中。沃尔特的故事是带有悲惨的个人主义的牺牲,凯蒂的故事是带有幸运的个人主义的新生。沃尔特自觉没有希望,在困顿的泥潭中深陷,他没有勇气去面对新的生活和新的世界,他将自己紧紧隐藏在自己的面纱之后,他活在自己为自己构建的信念世界之中,为自己的信念,"悲壮”赴死。凯蒂活在纷繁的社会中,从小我见大我,从假我见真我,从困局中得自由。她一步一步从虚伪的面纱下得见人性、社会冷暖以及新世界的希望,湄潭府之前的生活让她自省,湄潭府之行让她感知到一切,在死亡和黑暗中得到感悟和觉醒,得见光明,再次回到香港,欲望再次作祟,这一次她没有沉沦于虚妄的现实,她终于携手勇气做出了自己的决定,踏上了"离开”的归程。
故事的最后,面纱已落。凯蒂带着伤痕,牵着父亲的手,走向巴哈马的阳光。那里没有豪门的虚妄,没有激情的幻象,只有平凡而坚实的生活。这,或许就是毛姆留给所有在虚荣与迷茫中挣扎的人们,最温柔的答案:****唯有真实,可得自由。**** 故事的最后,面纱已落。凯蒂带着伤痕,牵着父亲的手,走向巴哈马的阳光。那里没有豪门的虚妄,没有激情的幻象,只有平凡而坚实的生活。这,或许就是毛姆留给所有在虚荣与迷茫中挣扎的人们,最温柔的答案:**_唯有真实,可得自由。_**
在《面纱》的故事中,沃尔特是极具“英雄主义”的个人,为自己固有的信仰慷然赴死;凯蒂是在纷杂的社会中逐步成长,在欲望和虚荣的交叉间度过早年,湄潭府一行,在黑暗中见光明,在死亡中见希望,在异国见真我,在“新世界”得自由。 在《面纱》的故事中,沃尔特是极具“英雄主义”的个人,为自己固有的信仰慷然赴死;凯蒂是在纷杂的社会中逐步成长,在欲望和虚荣的交叉间度过早年,湄潭府一行,在黑暗中见光明,在死亡中见希望,在异国见真我,在“新世界”得自由。
@@ -41,51 +46,28 @@ description: '面纱之下:理想的殉道者与真实的幸存者。'
------------------------------------------------------------------------ ------------------------------------------------------------------------
- 我不了解你,我一点儿都不了解你。 - 我不了解你,我一点儿都不了解你。
- 我知道你愚蠢、轻浮、没有头脑,但是我爱你。我知道你的目标和理想既庸俗又普通,但是我爱你。我知道你是二流货色,但是我爱你。想一想真是好笑,我竭力去喜欢那些讨你喜欢的东西,忍受折磨也要对你隐瞒起自己,实际上我并不无知粗俗、不爱散播丑闻也不愚蠢。我知道你何等害怕智慧,便尽我所能让你觉得我是个大傻瓜,跟你认识的其他人一样。我知道你嫁给我只图一时利益,我是那样爱你,我不在乎。大多数人,就我所知,当他们爱一个人,却没有得到爱的回报时就会觉得委屈不平,甚至愈发愤怒和痛苦。我不是那样,我从来没有指望你爱我,我看不出任何理由让你爱我,我也从来没想过自己会被人爱。我很感激能被允许爱你,当我时常想起你高兴跟我在一起,或者当我发现你眼中闪烁着愉快的爱意时,我就会欣喜若狂。我尽量不让我的爱来烦扰你,我知道那会让我承受不起,所以我一直察言观色,留意我的爱让你厌烦的最初迹象。大部分丈夫认为那是一种权力,我却准备当成恩惠来接受。 - 我知道你愚蠢、轻浮、没有头脑,但是我爱你。我知道你的目标和理想既庸俗又普通,但是我爱你。我知道你是二流货色,但是我爱你。想一想真是好笑,我竭力去喜欢那些讨你喜欢的东西,忍受折磨也要对你隐瞒起自己,实际上我并不无知粗俗、不爱散播丑闻也不愚蠢。我知道你何等害怕智慧,便尽我所能让你觉得我是个大傻瓜,跟你认识的其他人一样。我知道你嫁给我只图一时利益,我是那样爱你,我不在乎。大多数人,就我所知,当他们爱一个人,却没有得到爱的回报时就会觉得委屈不平,甚至愈发愤怒和痛苦。我不是那样,我从来没有指望你爱我,我看不出任何理由让你爱我,我也从来没想过自己会被人爱。我很感激能被允许爱你,当我时常想起你高兴跟我在一起,或者当我发现你眼中闪烁着愉快的爱意时,我就会欣喜若狂。我尽量不让我的爱来烦扰你,我知道那会让我承受不起,所以我一直察言观色,留意我的爱让你厌烦的最初迹象。大部分丈夫认为那是一种权力,我却准备当成恩惠来接受。
- 他知道你虚荣、懦弱、自私自利,他想让我亲眼看到这一切。他知道你会像野兔一样,危险一来就跑得远远的。他知道我深受蒙骗才会认为你爱上了我,因为他知道你不会爱任何人,只爱你自己。他知道你会牺牲我,好让自己毫发无损地逃脱出去。 - 他知道你虚荣、懦弱、自私自利,他想让我亲眼看到这一切。他知道你会像野兔一样,危险一来就跑得远远的。他知道我深受蒙骗才会认为你爱上了我,因为他知道你不会爱任何人,只爱你自己。他知道你会牺牲我,好让自己毫发无损地逃脱出去。
- 死亡既突然又无情,在一条条曲折的街巷上匆匆穿过。 - 死亡既突然又无情,在一条条曲折的街巷上匆匆穿过。
- 黎明乍现,阳光触到雾霭,令其闪闪发白,犹如雪之幽灵降至即将熄灭的星宿之上。 - 黎明乍现,阳光触到雾霭,令其闪闪发白,犹如雪之幽灵降至即将熄灭的星宿之上。
- 你瞧,这里只有你跟我踏踏实实在坚实的地上行走。修女们走在天上,而你丈夫则走在黑暗里。 - 你瞧,这里只有你跟我踏踏实实在坚实的地上行走。修女们走在天上,而你丈夫则走在黑暗里。
- 他的灵魂撕裂了,他一直活在一种虚假的构想之中,当真相击碎了幻象,他便认为现实本身也被击碎了。 - 他的灵魂撕裂了,他一直活在一种虚假的构想之中,当真相击碎了幻象,他便认为现实本身也被击碎了。
- 这个时辰的城市街巷空空荡荡,比任何时候都更像一座死城。路人一个个神色迷离,不禁让人怀疑他们全都是鬼魂。天空晴朗无云,初升的太阳将圣洁和煦的光芒洒满大地。很难想象,在这个愉悦、清新、爽朗的黎明,这座城市却像被疯子一手扼住咽喉的人,在瘟疫的黑暗掌控下苟延残喘。不可思议的是,当人类在痛苦挣扎、在恐惧中走向死亡时,大自然(蓝色的天空如孩童的心一般清澈)竟会如此无动于衷。 - 这个时辰的城市街巷空空荡荡,比任何时候都更像一座死城。路人一个个神色迷离,不禁让人怀疑他们全都是鬼魂。天空晴朗无云,初升的太阳将圣洁和煦的光芒洒满大地。很难想象,在这个愉悦、清新、爽朗的黎明,这座城市却像被疯子一手扼住咽喉的人,在瘟疫的黑暗掌控下苟延残喘。不可思议的是,当人类在痛苦挣扎、在恐惧中走向死亡时,大自然(蓝色的天空如孩童的心一般清澈)竟会如此无动于衷。
- 安宁只存在于人的灵魂中。 - 安宁只存在于人的灵魂中。
- 所有的人,乃至整个人类,就如同这条河里的水滴一样,流淌不定,一滴滴彼此接近,却又相距遥远,汇成一股无名的巨流奔向大海。既然一切转瞬即逝,任何事物都无关宏旨,人们竟还要荒唐地看重那些微不足道的事情,让自己也让别人遭受不幸,这实在太可悲了。 - 所有的人,乃至整个人类,就如同这条河里的水滴一样,流淌不定,一滴滴彼此接近,却又相距遥远,汇成一股无名的巨流奔向大海。既然一切转瞬即逝,任何事物都无关宏旨,人们竟还要荒唐地看重那些微不足道的事情,让自己也让别人遭受不幸,这实在太可悲了。
- 小小的两滴水随着河流默默流向未知,这两滴水相对而言是那样独特,但在旁观者的眼里,不过是河流中无法辨识的组成部分。 - 小小的两滴水随着河流默默流向未知,这两滴水相对而言是那样独特,但在旁观者的眼里,不过是河流中无法辨识的组成部分。
- 我又明白什么呢,生命是那么奇特。我觉得自己就像个一辈子都住在小池塘边上的人,突然间看见了大海,让我有点喘不过气来,但心里又充满了喜悦。我不想死,想活下去,于是感到一股新的勇气。我就好像那些老水手,起航驶向尚未发现的海洋,我的灵魂渴求未知的一切。 - 我又明白什么呢,生命是那么奇特。我觉得自己就像个一辈子都住在小池塘边上的人,突然间看见了大海,让我有点喘不过气来,但心里又充满了喜悦。我不想死,想活下去,于是感到一股新的勇气。我就好像那些老水手,起航驶向尚未发现的海洋,我的灵魂渴求未知的一切。
- 小小的两滴水默默地、默默地流向那黑暗、永恒的大海。 - 小小的两滴水默默地、默默地流向那黑暗、永恒的大海。
- 道。我们有些人在鸦片中寻找道,有些人在上帝那儿,有些人寻求威士忌,或去爱里寻找。这道终归只有一条,可它不通向任何地方。 - 道。我们有些人在鸦片中寻找道,有些人在上帝那儿,有些人寻求威士忌,或去爱里寻找。这道终归只有一条,可它不通向任何地方。
- 这一切既新奇又感人,她的心灵犹如一片平静而灰暗的大海,波浪起伏,蔚然壮观,阴郁浩瀚令人畏惧,突然间,一缕阳光投射过来,让一切变得活泼、友好而快乐。 - 这一切既新奇又感人,她的心灵犹如一片平静而灰暗的大海,波浪起伏,蔚然壮观,阴郁浩瀚令人畏惧,突然间,一缕阳光投射过来,让一切变得活泼、友好而快乐。
- 人生何其短暂,世界本来就充满了苦痛,人们却还要折磨自己,这岂不太可怜了吗? - 人生何其短暂,世界本来就充满了苦痛,人们却还要折磨自己,这岂不太可怜了吗?
- 她的性格就像一片乡野,初见时感觉辽阔而冷漠,但不久就会在巍峨山岭的褶皱间发现一个个掩映在果树丛中的欢腾的村庄,看见郁郁葱葱的草地上欢快流淌的一条条小河。这番宜人的景象会让你惊奇,甚至熨帖安心,但远处狂风劲吹的黄褐色高地尚不足以使你感到自如自在。 - 她的性格就像一片乡野,初见时感觉辽阔而冷漠,但不久就会在巍峨山岭的褶皱间发现一个个掩映在果树丛中的欢腾的村庄,看见郁郁葱葱的草地上欢快流淌的一条条小河。这番宜人的景象会让你惊奇,甚至熨帖安心,但远处狂风劲吹的黄褐色高地尚不足以使你感到自如自在。
- 只有一种办法赢得人心,那就是让自己成为人们会去爱的人。 - 只有一种办法赢得人心,那就是让自己成为人们会去爱的人。
- 我说不清。我不知道她们追求幻觉这一点是否真的那么重要,还是这种生活本身就很美好。我有个想法,唯一让我们有可能不带嫌恶地关注我们所生活的这个世界的,就是人类不断从混沌中创造出的美。他们描绘的画,他们谱出的乐曲,他们写的书,还有他们的生活。这一切中,最富于美的就是美好的生活,那是件完美的艺术品。 - 我说不清。我不知道她们追求幻觉这一点是否真的那么重要,还是这种生活本身就很美好。我有个想法,唯一让我们有可能不带嫌恶地关注我们所生活的这个世界的,就是人类不断从混沌中创造出的美。他们描绘的画,他们谱出的乐曲,他们写的书,还有他们的生活。这一切中,最富于美的就是美好的生活,那是件完美的艺术品。
- 道就是道路和行道者。那是一条永恒的路,所有的生命存在行走其上,但它并非由生命存在所创造,因为它本身便是生命存在。它什么都是,又什么都不是。万物由道所生,与道相符,最后万物又回归于道。它是一块方形却无四角,是种声音却不被聆听,是幅图画却未有形状;它是一张巨大的网,网眼阔如海洋,却什么都无法穿过;它是万物寻求庇护的避难所;它无处可寻,但你‘不窥牖’便可‘见天道’;它要人学会欲无所欲,让一切顺其自然。谦卑者尽得保全,屈身者终将直立。‘祸兮,福之所依;福兮,祸之所伏’;但谁能说清什么时候会出现转折点?追求柔慈之人会如小孩子一样平和。柔慈为进攻者获取胜利,为守卫者求得保全。战胜自己的人最为强大。 - 道就是道路和行道者。那是一条永恒的路,所有的生命存在行走其上,但它并非由生命存在所创造,因为它本身便是生命存在。它什么都是,又什么都不是。万物由道所生,与道相符,最后万物又回归于道。它是一块方形却无四角,是种声音却不被聆听,是幅图画却未有形状;它是一张巨大的网,网眼阔如海洋,却什么都无法穿过;它是万物寻求庇护的避难所;它无处可寻,但你‘不窥牖’便可‘见天道’;它要人学会欲无所欲,让一切顺其自然。谦卑者尽得保全,屈身者终将直立。‘祸兮,福之所依;福兮,祸之所伏’;但谁能说清什么时候会出现转折点?追求柔慈之人会如小孩子一样平和。柔慈为进攻者获取胜利,为守卫者求得保全。战胜自己的人最为强大。
- 自由!这便是一直萦绕在她心中的念头。尽管未来仍旧模糊,这一念头却像河上的薄雾,在清晨阳光的照耀下焕发虹彩。自由!不仅仅是挣脱烦恼的束缚,解除那让她消沉的伴侣关系的自由。自由!不仅是逃离死亡威胁的自由,更是逃离让她降低人格的爱情,逃脱所有精神束缚的自由,一种抽离出肉体的精神的自由。与自由相伴的,还有勇气,以及无论发生什么都毫不在乎的坚强气质。 - 自由!这便是一直萦绕在她心中的念头。尽管未来仍旧模糊,这一念头却像河上的薄雾,在清晨阳光的照耀下焕发虹彩。自由!不仅仅是挣脱烦恼的束缚,解除那让她消沉的伴侣关系的自由。自由!不仅是逃离死亡威胁的自由,更是逃离让她降低人格的爱情,逃脱所有精神束缚的自由,一种抽离出肉体的精神的自由。与自由相伴的,还有勇气,以及无论发生什么都毫不在乎的坚强气质。
- 她以为自己变了,以为自己意志坚强,会以一个冷静自持的女人的面目回到香港。心中掠过一个个崭新的念头,就像阳光下翻飞的黄色蝴蝶,让她对美好的未来充满期望。自由就像光的精灵召唤她,整个世界犹如一片广袤的平原,任她迈着轻快的步子昂首前行。她原以为自己已摆脱了肉欲和卑劣的情爱,足以过上一种纯洁健康的精神生活。她曾将自己比作黄昏时分悠然飞跃稻田的白鹭,它们就像安闲自处的头脑中翩然翱翔的片片思绪,可实际上她却仍是欲望的奴隶。 - 她以为自己变了,以为自己意志坚强,会以一个冷静自持的女人的面目回到香港。心中掠过一个个崭新的念头,就像阳光下翻飞的黄色蝴蝶,让她对美好的未来充满期望。自由就像光的精灵召唤她,整个世界犹如一片广袤的平原,任她迈着轻快的步子昂首前行。她原以为自己已摆脱了肉欲和卑劣的情爱,足以过上一种纯洁健康的精神生活。她曾将自己比作黄昏时分悠然飞跃稻田的白鹭,它们就像安闲自处的头脑中翩然翱翔的片片思绪,可实际上她却仍是欲望的奴隶。
- 我有希望,也有勇气。 - 我有希望,也有勇气。
- 你用面纱遮蔽他人的窥探,自己也难以一睹世界之实。 - 你用面纱遮蔽他人的窥探,自己也难以一睹世界之实。
------------------------------------------------------------------------ ------------------------------------------------------------------------
+10 -5
View File
@@ -1,11 +1,16 @@
--- ---
title: "泥潭" title: 泥潭
pubDate: 2026-02-09 pubDate: 2026-02-09
category: reading category: reading
tags: [read] tags:
mood: '#166534' - read
bookAuthor: '刘楚昕' pinned: false
description: '迷失于黑夜时,不妨抬头看看星空。' mood: '#686868'
bookAuthor: 刘楚昕
description: 迷失在黑夜中时,不妨抬头看看星空;如果不知道该相信什么,人应当面对自己的良知。
cover: ./assets/泥潭.png
hiddenTag: false
draft: false
--- ---
### 迷失在黑夜中时,不妨抬头看看星空;如果不知道该相信什么,人应当面对自己的良知。 ### 迷失在黑夜中时,不妨抬头看看星空;如果不知道该相信什么,人应当面对自己的良知。
+62
View File
@@ -0,0 +1,62 @@
---
title: "pi-mesh 全家桶详解:单机多智能体协同的四件套"
pubDate: 2026-11-01
category: tech
tags: [工程实践, AI 协作]
---
[pi](https://github.com/earendil-works/pi) 是我们日常用的编码智能体宿主。单开一个终端时它是个安静的助手;但同时开三四个终端、每个里面一个 agent 抢同一个仓库时,问题就来了:谁在线?谁在改哪个文件?消息发出去了对方到底看没看见?半夜跑着的构建谁有权停?
围绕 [pi-mesh](https://github.com/cgarrot/pi-mesh) 长出来的一族小工具把这层协同做成了基建。它们各管一摊,咬合成套:
![pi-mesh 工具族](./assets/pi-mesh-stack/stack.svg)
## 一、pi-mesh:通信骨干
pi-mesh 是一条本机总线:一个脱离会话存活的 broker(NDJSON 帧走 unix socket),每个 pi 终端以别名成为 peer,加入房间(room)。消息 < 50ms 到达,零运行时依赖,Node ≥ 20。
它最值得称道的是**诚实状态模型**:`delivered`(写进了对方套接字)≠ `read`(注入了对方会话)≠ `answered`(拿到了显式回复)——三者永不混淆,没有一个是"完成"的意思。配套的**持久台账是 hash-only 的**:消息正文永不落盘,重启后能审计"谁在何时给谁发过什么",但看不到内容。这个自我约束后来成了整个工具族的设计公理:**mesh 只做路由器,永不做数据库。**
协同刚需有三个都在骨干里:
- **`mesh_reserve` 路径预约**:改文件前先预约,其他 peer 的 `edit`/`write` 工具在该路径上直接被拦——并发编辑靠机制防,不靠喊话。
- **`mesh_claim` 任务租约**:单写者锁,带 `epoch:fence`(broker 重启即作废旧围栏);提交信息里写 `Mesh-Lease: task#leaseId#epoch:fence`,配套 commit-msg 钩子直接拒绝过期围栏的提交。
- **能力路由 `cap:`**:`mesh_send({ "to": "cap:deploy", ... })` 把任务发给任意一个空闲且自报该能力的 peer,而不是等忙着的队友。消息优先级三档:normal 排队、urgent 插队、force 先中止对方当前回合再送达(force 属管控类,必须带 reason)。
另有 `mesh_wait_all` 做群裁(一次收回“谁答了、谁没答”的名单)、`mesh_status` 看 peer 忙闲(● 干活 / ○ 空闲 / ✕ 卡死)、`pimesh` CLI 支持无会话接入(`pimesh attach` 能收养死掉会话的身份)。跨机部署也支持:一台起 broker,其余机器做客户端。
诚实的颗粒度细到退出码:CLI 里 3 表示 `expired`——晚到的答复仍会送达,只是你等起见超时了;5 表示租约已过期,`commit-msg` 钩子据此把提交拒之门外。甚至 ESC 清空消息队列这种宿主边界也考虑到了:批量注入若撞上回合中止,broker 会在该回合结束后经中止安全通道重投(有界重试),状态页能看到“中止清除恢复重投”计数。
## 二、pi-board:团队记忆板
消息是易逝的,台账是 hash-only 的,那"我们学到了什么"存哪?[pi-board](https://github.com/wait4xx/pi-board) 补上这层。五种类型化条目:
- **OBSERVED**——看到了什么(不承诺)
- **FACT**——验证过的事实
- **FAIL**——一条被证伪的路(价值最高:全队不必再烧第二次预算)
- **CLAIM**——认领声明,与 `mesh_claim` 租约配对(板只记录,从不执行)
- **PATCH_SUMMARY**——落了什么改动
设计上处处是“有界与诚实”:append-only 本地存储、refuse-not-evict(超限拒绝写入而非静默淘汰)、条目默认短小、长文 `detail` 只能按 id 展开、布尔检索(`,` = OR,`&` = AND)覆盖全史而 `board_read` 只看近期窗口;检索无果时返回诚实的空,绝不猜一条充数。写入以普通广播帧发进房间,turn 边界有新 FAIL 摘要行;房间角色 member / observer 沿用 mesh 语义,只读 peer 永远不会在重连后被误升为写者。它以普通 peer 身份入房——**存储全在板自己家里,broker 里零持久化**。附带的 `piboard` CLI 五个动词一把梭:join(member / observer)/ watch / write / read / grep,一动词一动作,无会话的终端也能进房只读围观。这套类型学与"小条目+按需展开+近期记忆/全史检索"的分工直接来自学术论文(血统故事见[下一篇](/archives/agensh/))。
## 三、pi-jobd:后台作业守护
`bash` 会阻塞当前回合,`/reload` 会杀进程。[pi-jobd](https://github.com/cgarrot/pi-jobd) 把长命令放进独立进程组,由 per-user 守护监管:~150ms 返回,作业存活于会话之外。什么时候用它?三条判据:命令预计超过 60-90 秒;你必须边跑边干别的;或者它得扛过 `/reload` 和会话结束——以及"别的 agent 应该能停它"这一条:`mesh_job_control` 让任一授权 peer 远程 stop/pause/resume 别人的后台作业。
状态机同样诚实:`running / paused / stopped / exited / failed`,外加一个 `lost`——守护重启后无法证明 pid 归属时,宁可拒绝 stop 也绝不信号一个来路不明的进程。这个包还有个巧思:装包不够,模型还得**知道什么时候用**——它靠 prompt 快照、七个工具上的决策守则和一份按需加载的 skill 三层把使用时机喂给模型。
## 四、pi-subagents:worker 控制
[pi-subagents](https://github.com/nicobailon/pi-subagents) 管的是进程内子代理:前台子代理流式跑在对话里,后台子代理活在独立的 runner 进程中随时可查。装完即用,不需要学斜杠命令——直接说"用 reviewer 审这个 diff"就行。它预置了 scout(侦察)、reviewer(审查)这类常用角色,也支持保存的工作流和并行审查。与 mesh 的接口是 `mesh_run_control`:远程 stop 或 steer 一个 peer 的 RPC worker。
## 五、四件套的一次合奏
![协作循环](./assets/pi-mesh-stack/loop.svg)
## 六、上手五分钟
安装各一行:mesh 与 subagents 走 pi 包管理——`pi install npm:pi-mesh-extension`、`pi install npm:pi-subagents`;pi-board 还没发 npm,源码三步(clone、`npm ci`、`npm run build`),且可 CLI 独立用(不需要 mesh 也能读写本地板);pi-jobd 同样源码构建,`pijob run / list / log / stop` 开箱即用。装完在同一目录开两个 pi 会话,就是两个 peer,`mesh_status` 互相可见——协同基建的“零配置”不是口号。
对话走 mesh,记忆上 board,长活交 jobd,排他靠租约——四个包各司其职,没有任何一个越界。这个博客的重设计期就是这套东西的实战场:两个 mesh 终端(op-gitea、op-artalk)与主线并行,一天 19 连发;发布链中断的修复窗口里,主线预约了 build.sh 紧急处置,另一边赶去补刀的编辑被预约机制正确拦下——撞车靠规则防住,不靠谁手快。
四件套加起来没有一个数据库、没有一个中心大脑,却把"谁在线、谁在干啥、学到过啥、长活谁看着"四件事全部钉死。小工具、硬边界、诚实状态——这套哲学比任何一个具体包都值得抄。
+10 -5
View File
@@ -1,11 +1,16 @@
--- ---
title: "赎罪" title: 赎罪
pubDate: 2026-02-26 pubDate: 2026-02-26
category: reading category: reading
tags: [read] tags:
mood: '#9f1239' - read
bookAuthor: '伊恩·麦克尤恩' pinned: false
description: '因一个人、一张图、一句台词,看了一本书。' mood: '#bf002f'
bookAuthor: 伊恩·麦克尤恩
description: 亲爱的塞西莉亚,我们缘分未尽
cover: ./assets/赎罪.png
hiddenTag: false
draft: false
--- ---
### 你要回来,我会等你。 ### 你要回来,我会等你。
+61
View File
@@ -0,0 +1,61 @@
---
title: "Spec Harness 详解:把长程开发装进可验收的任务包"
pubDate: 2026-11-01
category: tech
tags: [工程实践, AI 协作]
---
AI 编程智能体最不缺的是写代码的带宽,最缺的是记忆。一个跨了好几轮会话的任务,改到一半的方案、已经否决的路线、验收口径,散落在聊天记录里就等于丢了——换一个会话、重启一次终端,上下文归零。[Spec Harness](https://github.com/MicTx/spec-harness) 解决的就是这个问题:它是一个面向 AI 编程智能体的任务包工作流工具,把长程改动保存为**可恢复、可验证的 Development Record**,用范围、证据和 Git 门禁管住全程。它不提供模型,也不替你做产品决策。
一句话概括它的世界观:**当一个编码任务的寿命超过一段对话,缺的不是更聪明的模型,而是一份大家(包括未来的你)都能读的合同。**
## 一、核心抽象:任务包
所有工作装进 `.spec/specs/YYYY-MM-DD_slug/` 目录,核心是两个文件:`spec.md` 写范围、假设与取舍(还有一节很特别的"已证伪路径",后面细说),`tasks.md` 写任务清单。收尾后整包归档到 `.spec/specs/archive/`,归档目录本身就是项目的工程日志。
每个任务必须带两行合同——**boundary**(这个任务能动哪些文件)和 **verify**(怎么证明做完了)。verify 是只读断言,跑出退出码 0 才许勾选。拿这个博客真实归档的 `2026-10-26_blog-5f` 包里 T3 任务举例:
![任务包解剖](./assets/spec-harness/package-anatomy.svg)
boundary 划定"能动什么",verify 划定"怎么证明做完"——两行齐备,缺一不勾。这不是形式主义:多 agent 并行时,boundary 就是车道之间的护栏(我们的实践里,全局样式文件只许主线动,车道要新变量走申请);verify 则把"看起来好了"和"证明做完了"区分开,口头"过了"永远不算数。
`spec.md` 里最有味道的一节是**已证伪路径**:被否决的方案连否定理由一起入库。我们的 blog-5f 包里记着三条——首屏 banner 化(视觉裁定否决)、热力图换绿(破坏灰阶骨架)、标题 27px 收到 22px(层级锚点,要轻减字重不减号)。下次谁想再试一遍,先读档案,少烧一轮预算。
## 二、阶段流水线:五个核心命令与它们把守的门
![阶段流水线](./assets/spec-harness/pipeline.svg)
五个核心命令串起一条可验证链:**目标 → 范围 → 任务 → 验证 → 归档 → 提交 → 推送**。每个阶段之间都有门把守:`spec.md` 有模板占位符就拒绝进入 run;`done` 要求每个任务 boundary/verify 齐备、验收面全绿且至少有一条非占位证据(脚本输出、测试结果);`push` 只合并 `spec/` 前缀的集成分支,文件改动与提交永远分开做,禁用 `--no-verify`。
被阻塞的任务会打 `!` 标记上浮给用户,绝不静默跳过;复核轮(check)发现的可执行问题必须当轮写回任务包并修复,不允许把"待办"当交付物甩给用户。
**中断恢复是常态设计**——这是它对“任务寿命超过对话”最直接的回答:任意时刻重跑 `/spec:run`,从 `tasks.md` 的勾选状态续跑,不依赖任何对话记忆。更进一步,`/spec:autoplan` 做交互式规划(澄清目标后过框架、细节、复核三轮,规划就绪即停,不自动开工),`/spec:autorun` 按规划逐包执行,默认最多 5 轮自动链(`--max-rounds` 可调),暂停后从同一步恢复;一键式入口 `/spec:goal` 则把规划、执行、复核、归档、提交一次跑完。
命令面共 12 个阶段命令:core 档装五个核心命令,full 档补齐 goal / autoplan / autorun / update / status / doctor / organize 七个扩展件——`doctor` 做环境自检与修复,`organize` 做结构审计。还有一个容易被忽略的细节:安装只是放文件,不会主动逐任务评估;想让它在你动手前主动问一句“这事要不要开包”,需要显式接线一次 `install_activation_hooks.py --mode on`。判据看交付形状——独立验收面、实际实施者、可恢复的在途任务;小修、只读调查、一次性实验都直接做。它防的是“忘了评估”,不防刻意瞒报——文档里对此毫不讳言。
## 三、多 agent 编排:一个编排者,有边界的帮工
Spec Harness 对多 agent 的态度很克制:**主会话是唯一的编排者**,路由、集成、验收和 `done`/`push` 门禁永远不出主线。旁支工作(探索、构建、审查这类上下文无关的活)可以委托,但要先过机器路由决策(`scripts/route_decision.py`)并写下任务合同:目标、范围、排除区、产出、验证方式五项齐备才许派。它甚至为"跨会话交接"准备了可选的 `handoff.md`——生成式快照头加 append-only 条目日志,单人、多人、agent、集群四种协作模式各有一套必填小节。
扩展位(slots)机制则把"别的团队流程"插进来:`team-loop` 管团队循环、`workflow-runner` 管工作流扇出,各自带 README 协议和测试。源码库里还附了 orchestrator/planner/reviewer/confirmer 四个编排代理定义。
## 四、安装:一个安装器,十余个宿主
Spec Harness 是 Python 3.9+ 的标准库实现(PolyForm Noncommercial 许可),安装就三步:
```bash
git clone https://github.com/MicTx/spec-harness.git /tmp/spec-harness
cd /tmp/spec-harness
git checkout v0.22.2
bash install.sh
```
安装器支持十余个宿主:Claude Code、Codex、Gemini CLI、Grok、OpenCode、OpenClaw、Hermes、Pi、ZCode、DeepSeek Harness、Devin、Cursor……不指定时自动探测已装的宿主。安装档位分 core / full:core 只装五个核心阶段命令加 Git 钩子;full 补齐全部 12 个阶段命令和 slots、agents 扩展组件。我们在 Pi 上用 `INSTALL_HOSTS=pi bash install.sh`,装完 `/spec` 系列命令直接可用。
## 五、跑在一个真实博客上
这个博客就是它的用户。归档目录里躺着从裸 clone 构建改造、移动端布局修复到 SEO 解封的一串任务包:blog-5f 一个包八个任务、三条车道(两条 mesh 终端加主线)并行,每个任务的 verify 都是能在命令行跑出退出码的断言,页面类任务统一挂 E2E——`E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs`,34 项全过才算数。包与包接力,一天 19 个提交全绿落地。
门禁也真的拦过烂账:一次一个全 manual、从未执行的任务包混进了 `.spec/specs/`,check 门禁直接判 blocked,连带拦下所有携带归档路径的提交——直到把它移出。门禁防的不只是烂尾,还有“看起来有规划”的空包。
工具会迭代,但这套思路不会过期:**把"要干什么"写成合同,把"做完了"写成证据,把过程归档成日志。** 无论协作方是人还是 agent,这三件事都是长程工程的地基。
+128
View File
@@ -0,0 +1,128 @@
---
title: 写作模板:这篇文章本身就是模板
pubDate: 2026-11-02
category: tech
tags:
- 写作
- 模板
pinned: true
description: 一篇文章演示全站可用的排版能力:标题层级、彩色强调、字族、引用、代码窗、表格、图、链接卡、折叠。复制它开始写新文。
hiddenTag: true
draft: false
---
这篇不是教程,是**可复制的模板**。下面每一节都在真跑——你看到的效果,就是 Markdown 源码能产出的效果。写新文章时,把本文件复制一份、改个 slug、删掉不需要的段落即可。
它同时是**置顶功能**的活样本:`frontmatter` 里的 `pinned: true` 会让它排在首页、归档与书架的最前面,而日期、URL、订阅源里的时间都不变。
## 一、标题层级与段落
正文用 `##` 作一级节标题,`###` 作二级。**不要用 `#`**——文章标题由 frontmatter 的 `title` 渲染,正文再写 `#` 会出现两个一级标题,屏幕阅读器与搜索摘要都会困惑。
段落之间空一行即可。行内可以**加粗**、*斜体*、~~删除线~~、`行内代码`,也可以放[普通链接](https://astro.build)。
## 二、彩色强调:想用多少用多少
颜色现在**没有次数限制**(2026-11-02 起取消了原来的「一页最多三处」)。唯一还守着的是外壳:导航、页脚、按钮、背景保持灰阶——外壳是气质,正文是表达。
<span class="c-blue">第一处:一个关键结论用蓝色提出。</span> 后面照常叙述。
<span class="c-red">第二处:需要警示的坑用红色。</span> 比如「改完 Markdown 插件必须清 `node_modules/.astro`,否则改动静默不生效」。
<span class="c-green">确认可行的路径用绿色。</span> 想让整段都是重点也不必克制,把它们都标出来即可。
可用色相共 8 组(blue / teal / green / yellow / orange / red / pink / purple),每组有标准与 `-soft` 两档;另有 4 个灰阶层级类(`ink-muted` / `ink-warm` / `ink-cool` / `ink-faint`)用于降调,不算彩色额度。
## 三、字族:换字体只作用于被包裹的文字
正文默认是无衬线。想给某句话换气质,用类包起来就行:
<span class="font-kai">这一句是楷体。适合题记、书评里引的原句,或者需要慢一点读的段落。</span>
<span class="font-huiwen">这一句是汇文明朝体。长段落用它会明显降低阅读速度,所以只用在短句上。</span>
可用字族共 6 族(`font-kai` 楷、`font-song` 宋、`font-hei-sys` 黑、`font-fangsong` 仿宋、`font-huiwen` 汇文明朝、`font-kuaile` 快乐体),另有 `font-art-kuaile` 艺术字标题类。**注意体积**:整页换字族会让访问者下载一份中文字体,正文默认字体不要轻易改。
### 三级标题长这样
比二级小一档、加粗、上下留白更紧。用于把一节拆成几个小话题。
## 四、引用与列表
> 引用块用于摘录别人的话。署名可以换成斜体放在最后。
>
> —— 一句虚构的署名
无序列表:
- 第一项
- 第二项,可以带 `行内代码`
- 第三项,可以嵌套:
- 子项一
- 子项二
有序列表适合步骤:
1. 复制模板
2. 改 frontmatter 的 slug 与标题
3. 删掉不需要的段落
## 五、代码块
四行及以上的代码块会带上灰阶窗条(三个点),短代码块不加——避免小片段看着像截图。
```ts
// 排序契约:置顶优先 → 时间倒序 → id 兜底
export function sortPosts<T extends Post>(posts: T[]): T[] {
return [...posts].sort((a, b) => {
const pin = Number(b.data.pinned === true) - Number(a.data.pinned === true);
return pin !== 0 ? pin : b.data.pubDate.getTime() - a.data.pubDate.getTime();
});
}
```
行内提到变量时用 `posts` 这种形式即可。
## 六、表格
| 能力 | 写法 | 注意 |
| --- | --- | --- |
| 彩色强调 | `<span class="c-blue">` | 不限次数(外壳仍灰阶) |
| 换字族 | `<span class="font-kai">` | 整页换会拉字体体积 |
| 图片 | `![说明](./assets/<slug>/x.jpg)` | 体积纪律见写作指南 |
| 视频 | frontmatter `video:` | 放 public/media,≤50MB |
| 链接卡片 | 裸链接单独成段 | 构建期抓 OG,失败回落普通链接 |
## 七、图片
图片用相对路径引用,挂在 `content/posts/assets/<slug>/` 下,点击可放大(矢量图不糊):
![示例配图:一片灰阶海岸](./assets/media-sample/sample-photo.jpg)
## 八、链接卡片
一个裸链接单独成段,构建期会抓取 OG 预览生成卡片;抓不到就退回普通链接,不会坏版:
https://astro.build
## 九、折叠长内容
长附录、完整日志、大段配置可以折叠,零 JS,原生 `details`:
<details>
<summary>展开看一个被折叠的长例子</summary>
折叠区里可以放任何东西——段落、列表、代码块。
- 适合放「按需查看」的细节
- 不适合放结论(读者可能永远不展开)
</details>
## 十、分隔线之后收尾
---
收尾三句就够:这篇讲了什么、读者该带走什么、下一步去哪。模板不是要你写满十节,而是**在你需要表达时,能力都在手边**。
下一篇开始,复制本文件,删到只剩你要说的。
+153
View File
@@ -0,0 +1,153 @@
# 关于页(后台「页面文案 → 关于页」可改)
# ── 一级:blocks 块式正文(按顺序渲染,可拖拽排序;某块 enabled: false 即隐藏)
# ── 二级:intro 多段 markdown(仅在 blocks 为空时使用)
# ── 三级:内置默认文案(两者都空时兜底,构建永不炸)
layout:
# 名片区(头像/名字/身份语/社交)开关:头像等数据事实源在「主页设置」
showIdCard: true
blocks:
- type: prose
width: narrow
body:
zh: |
<span class="font-huiwen c-red">写作者</span> · 读者 · 快门按压者,拥抱开源。
这里是我三条轨道的公开工作台:**读过的书**留下评注,**写过的代码**进项目墙,**按下的快门**收入图库。
所有内容都有一个明确去处:
- 长文沉淀在文章与书架
- 短句与随手拍落在瞬间
- 影像收进图库,配一点 EXIF 与一句策展
本站本身即开源,构建与发布全链都在仓里。
en: |
<span class="font-huiwen c-red">Writer</span> · Reader · Shutter-presser, embracing open source.
This is the public workbench of my three tracks: **books** that left notes, **code** that reaches the project wall, and **shutters** I pressed that land in the gallery.
Everything has a clear destination:
- Long-form writing goes to posts and the bookshelf
- Short lines and snapshots land in moments
- Images go to the gallery, with a bit of EXIF and one line of curation
This site is itself open source — the full build and deploy pipeline lives in the repo.
- type: tracks
title:
zh: 兴趣版图
en: Interests
items:
- icon: book
name:
zh: 阅读
en: Reading
desc:
zh: 文学与戏剧为主,读到哪写到哪。
en: Mostly literature and drama; I write as I read.
- icon: terminal
name:
zh: 技术
en: Tech
desc:
zh: Linux、容器与自托管,把家里的小机器折腾成顺手的工具。
en: Linux, containers, and self-hosting — turning the little machines at home into handy tools.
- icon: camera
name:
zh: 摄影
en: Photography
desc:
zh: 街拍与山野,随身一台无反,在等光的间隙按快门。
en: Street and landscapes with a mirrorless camera, pressing the shutter while waiting for the light.
- type: prose
title:
zh: 关于我
en: About me
body:
zh: |
气象是我起点的专业——一个想做预报的人,依旧把云图当日常读物。
写代码是第二条主线:Linux、容器、自托管,把日常工具一件件磨顺手。
作品大多在 [wait4x.online](https://wait4x.online/)。
信条:**拥抱开源,热爱开源**。
en: |
Meteorology is where I started — an aspiring forecaster who still reads weather charts for fun.
Coding is the second thread: Linux, containers, self-hosting — polishing everyday tools one by one.
Most of my work lives at [wait4x.online](https://wait4x.online/).
Motto: **embrace open source, love open source**.
- type: linklist
title:
zh: 找到我
en: Find me
items:
- label:
zh: 本站
en: Website
href: 'https://wait4x.online/'
note:
zh: 文章、图库与瞬间
en: Posts, gallery and moments
- label:
zh: 邮件
en: Email
href: 'mailto:X@alumni.nudt.edu.cn'
note:
zh: 有话直说
en: Say hello directly
- label:
zh: GitHub
en: GitHub
href: 'https://github.com/wait4xx'
note:
zh: 开源项目与提交
en: Open-source projects and commits
- label:
zh: 自托管 gitea
en: Self-hosted gitea
href: 'https://git.wait4x.online/wait4x/blog'
note:
zh: 本站源码
en: Source of this site
- label:
zh: RSS
en: RSS
href: /rss.xml
note:
zh: 订阅更新
en: Subscribe to updates
- type: prose
title:
zh: 本站如何构建
en: How this site is built
body:
zh: 页面由 Astro 静态生成,不含运行时框架 JS;内容住在自托管 gitea——本地编辑器与 gitea 网页写的是同一个 git 仓库,push 之后由 homelab 自动构建发布;评论走自托管 Artalk。页脚 built from 一行链到本次构建对应的 commit,构建过程透明可追溯。
en: Pages are statically generated by Astro with no runtime framework JS. Content lives on a self-hosted gitea — my local editor and the gitea web UI write to the same git repository, and each push is built and published automatically by the homelab. Comments run on self-hosted Artalk. The built-from line in the footer links to the commit behind this build, keeping the process transparent and traceable.
# ---- 二级回退(blocks 为空时使用)----
intro:
zh: |
写作者 · 读者 · 快门按压者,拥抱开源。
这里是我三条轨道的公开工作台:**读过的书**留下评注,**写过的代码**进项目墙,**按下的快门**收入图库。
en: |
Writer · Reader · Shutter-presser, embracing open source.
This is the public workbench of my three tracks: **books** that left notes, **code** that reaches the project wall, and **shutters** I pressed that land in the gallery.
# 页面级排版覆盖(层 2)
# 页面级排版覆盖(后台「关于页 → 页面排版覆盖」可改):
# 整页换字族会带来字体体积成本(如 font-huiwen 整页 ≈694KB),默认留空=全站默认排版
# typography:
# font: font-huiwen # 字族:font-serif/font-sans/font-kai/font-huiwen/font-kuaile…
# accent: c-red # 强调色类:c-blue/c-red/…(外壳仍灰阶)
# scale: 1.05 # 字号缩放 0.8–1.25
+18
View File
@@ -0,0 +1,18 @@
tagline:
zh: 拥抱开源,热爱开源
en: Embracing open source, loving open source.
latestPhotos: 4
perTrackLimit: 6
sections:
tracks: true
photos: true
heatmap: true
projects: true
favicon: /favicon.svg
socials:
- label: GitHub
url: https://github.com/wait4xx
icon: github
- label: 邮件
url: mailto:X@alumni.nudt.edu.cn
icon: email
+19
View File
@@ -0,0 +1,19 @@
typing:
- zh: 气象出身,写代码为乐
en: Trained in meteorology, coding for fun
- zh: 自托管一切能自托管的东西
en: Self-hosting whatever can be self-hosted
- zh: 拥抱开源,热爱开源
en: Embrace open source, love open source
blurb:
zh: 本站本身即开源:
en: 'This site is itself open source:'
repo_url: https://git.wait4x.online/wait4x/blog
repo_label: git.wait4x.online/wait4x/blog
blurb_after:
zh: —— Astro SSG、双写作流、构建发布全链皆在仓中。以下是我在自托管 gitea 与 GitHub 上的全部公开仓库,以及最近一年的贡献活动。
en: — the Astro SSG, dual-writing workflow, and the full build & deploy pipeline live in the repo. Below are all my public repositories on self-hosted gitea and GitHub, plus contributions from the past year.
mode: all
sort: pinned-first
perSource: github
intro_from: 'off'
+37
View File
@@ -0,0 +1,37 @@
# 主题旋钮(样式即数据 · 层 4):只放白名单内的 9 组旋钮。
# 红线:--measure(版心)与断点/纸墨/code 配色不在此列——它们牵动版面结构与分轨身份。
# 全部留空 = 沿用 src/styles/tokens.css 的既有值(零配置零变化)。
# 钳制与单调性在 src/lib/theme.ts 里做:越界值被夹取而非报错,构建不会因主题配置失败。
# ① 强调色(明/暗两值;构建期闸门校验正文对比度 ≥4.5,不达标回落默认)
accent_light: '#3454d1'
accent_dark: '#8ea2ff'
# ② 图片圆角(4–16;08 文档裁定上限 14)
radius_img: 10
# ③ 标题字号(22–32;必须 h1 > h2 > h3,否则整体回落默认)
text_h1: 27
text_h2: 22
text_h3: 19
# ④ 正文字号(16–22)
prose_size_reading: 20
prose_size_tech: 19
# ⑤ 正文行高(1.5–2.0)
leading_reading: 1.75
leading_tech: 1.7
# ⑥ 间距梯度(8–80;必须严格递增 3 < 4 < 5 < 6,否则回落默认)
space_3: 16
space_4: 24
space_5: 40
space_6: 64
# ⑦ 段落流距(0–2em)
flow_p: 0.9
flow_h2_post: 0.6
# ⑧ 过渡时长(150–600ms)
dur: 350
+314
View File
@@ -0,0 +1,314 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>灯箱 v2.1 提案预览 · wait4x</title>
<style>
/* ===== 灰阶基底(对齐站 tokens.css:四档灰 + 唯一曲线族) ===== */
:root{
--bg:#fff; --bg-soft:#f7f7f8; --ink-1:#111113; --ink-2:#33343a; --ink-3:#6b6e78;
--ink-4:#a0a3ad; --line:#e6e7eb; --radius-img:10px;
--mono:"JetBrains Mono",ui-monospace,Consolas,monospace;
--sans:"Noto Sans SC Variable","Noto Sans SC",system-ui,-apple-system,"PingFang SC",sans-serif;
--ez:cubic-bezier(.4,0,.22,1); /* PhotoSwipe 333ms 节奏同族曲线 */
}
*{box-sizing:border-box}
body{margin:0;background:var(--bg);color:var(--ink-2);font-family:var(--sans);}
body.locked{overflow:hidden}
.wrap{max-width:896px;margin:0 auto;padding:28px 20px 72px}
kbd{font-family:var(--mono);font-size:11.5px;border:1px solid var(--line);background:#fff;
border-radius:4px;padding:1px 6px;color:var(--ink-3)}
/* ===== 顶部说明:这是提案预览稿 ===== */
.note{background:var(--bg-soft);border:1px solid var(--line);border-radius:10px;
padding:14px 18px;font-size:13.5px;line-height:1.7;color:var(--ink-2);margin:18px 0 30px}
.note strong{color:var(--ink-1)}
.note ul{margin:6px 0 0;padding-left:18px}
.note li{margin:2px 0}
h1{font-size:27px;color:var(--ink-1);margin:8px 0 2px;font-weight:600}
.sub{font-family:var(--mono);font-size:12.5px;color:var(--ink-4)}
/* ===== 影像墙:cover 裁切(与线上一致,起飞动画的前提) ===== */
.stage{background:rgba(255,255,255,.04);border-radius:6px;padding:14px}
.wall{list-style:none;margin:0;padding:0;display:grid;grid-template-columns:repeat(3,1fr);
grid-auto-rows:8px;grid-auto-flow:dense;gap:14px}
.shot{position:relative;overflow:hidden;border-radius:var(--radius-img);grid-row-end:span var(--sp,13)}
.shot button{display:block;width:100%;height:100%;padding:0;border:none;background:none;cursor:zoom-in}
.shot img{display:block;width:100%;height:100%;object-fit:cover}
@media (max-width:820px){.wall{grid-template-columns:repeat(2,1fr)}}
@media (max-width:560px){.wall{grid-template-columns:1fr 1fr;gap:10px}}
/* ===== 灯箱:fixed 覆盖层(生产将等价几何搬回原生 <dialog>) ===== */
.lbx{position:fixed;inset:0;z-index:80}
.lbx[hidden]{display:none}
.lbx-bg{position:absolute;inset:0;background:rgba(8,10,14,.96);opacity:0;
transition:opacity 300ms var(--ez)}
.lbx.on .lbx-bg{opacity:1}
/* 双盒:win=裁切窗(动画 left/top/w/h),pic=内图(动画几何 + 缩放 transform) */
.lbx-win{position:fixed;overflow:hidden;border-radius:6px;transform-origin:0 0;
box-shadow:0 12px 48px rgba(0,0,0,.5);touch-action:none}
.lbx-pic{position:absolute;transform-origin:50% 50%;user-select:none;-webkit-user-drag:none;
transition:transform 90ms linear;cursor:zoom-in;display:block}
.lbx.landing .lbx-win,.lbx.landing .lbx-pic{
transition:left 300ms var(--ez),top 300ms var(--ez),width 300ms var(--ez),
height 300ms var(--ez),transform 300ms var(--ez),opacity 300ms var(--ez)}
.lbx-pic.zoomed{cursor:grab}.lbx-pic.grabbing{cursor:grabbing}
.lbx-pic.swap{opacity:0}
/* chrome 层 1:关闭/箭头/计数(+140ms 淡入) */
.c1{opacity:0;transition:opacity 220ms var(--ez)}
.lbx.on .c1{opacity:1;transition-delay:140ms}
.lbx-btn{position:fixed;width:44px;height:44px;border-radius:10px;cursor:pointer;
border:1px solid rgba(255,255,255,.22);background:rgba(20,24,30,.55);color:#e6e9ee;
backdrop-filter:blur(4px);box-shadow:0 2px 10px rgba(0,0,0,.35);display:grid;place-items:center}
.lbx-btn:hover{background:rgba(255,255,255,.12)}
.lbx-close{top:16px;right:16px}
.lbx-prev{left:14px;top:50%;translate:0 -50%}
.lbx-next{right:14px;top:50%;translate:0 -50%}
.lbx-count{position:fixed;top:26px;left:20px;font-family:var(--mono);font-size:12.5px;color:#98a2b3}
/* chrome 层 2:caption / title+EXIF / 动作(+300ms 淡入) */
.c2{opacity:0;transition:opacity 260ms var(--ez)}
.lbx.on .c2{opacity:1;transition-delay:300ms}
.lbx-meta{position:fixed;left:0;right:0;bottom:0;display:flex;flex-direction:column;align-items:center;
gap:3px;padding:14px 72px 20px;text-align:center;text-shadow:0 1px 4px rgba(0,0,0,.6)}
.lbx-caption{margin:0;font-family:var(--sans);font-size:13.5px;line-height:1.6;color:#dfe6f3;max-width:640px}
.lbx-row{display:flex;align-items:center;flex-wrap:wrap;justify-content:center;gap:14px;
font-family:var(--mono);font-size:12.5px}
.lbx-title{color:#fff;font-family:var(--sans);font-size:13px;font-weight:600}
.lbx-exif{color:#98a2b3}
.lbx-acts{display:inline-flex;gap:10px}
.lbx-act{display:inline-flex;align-items:center;gap:5px;padding:6px 11px;border-radius:10px;
border:1px solid rgba(255,255,255,.22);background:rgba(20,24,30,.55);color:#dfe6f3;
font-family:var(--mono);font-size:12px;text-decoration:none;backdrop-filter:blur(4px)}
.lbx-act:hover{background:rgba(40,48,60,.75);color:#fff}
/* 沉浸态:zoom>1 或按 i —— chrome 全隐,舞台纯净 */
.lbx.clean .c1,.lbx.clean .c2,.lbx.noui .c1,.lbx.noui .c2{opacity:0;transition-delay:0ms}
/* 首开提示 + 缩放指示 */
.lbx-hint{position:fixed;left:50%;bottom:128px;translate:-50% 0;padding:7px 13px;border-radius:10px;
border:1px solid rgba(255,255,255,.22);background:rgba(20,24,30,.55);backdrop-filter:blur(4px);
font-family:var(--mono);font-size:12px;color:#dfe6f3;opacity:0;transition:opacity 400ms var(--ez);
pointer-events:none}
.lbx-hint.show{opacity:1}
.lbx-zind{position:fixed;right:20px;bottom:128px;font-family:var(--mono);font-size:12px;color:#98a2b3;
opacity:0;transition:opacity 300ms var(--ez);pointer-events:none}
.lbx-zind.show{opacity:1}
@media (max-width:560px){
.lbx-prev{left:6px}.lbx-next{right:6px}
.lbx-meta{padding:12px 16px 16px}
.lbx-hint{bottom:150px}
}
@media (prefers-reduced-motion:reduce){
.lbx-bg,.c1,.c2,.lbx-pic,.lbx-win,.lbx-hint,.lbx-zind{transition:none!important}
}
</style>
</head>
<body>
<div class="wrap">
<h1>图库 · 灯箱 v2.1 提案预览</h1>
<p class="sub">预览稿 —— 交互为提案演示(非线上行为)· 图片为 content/photos 实拍,EXIF 为示例数据</p>
<div class="note">
<strong>这是给浏览器直接打开的交互演示</strong>,对应《09-灯箱美化探索》提案 #1/#2/#3/#6/#7:
<ul>
<li><strong>原位起飞开合</strong>:点击缩略图,照片从原位放大入舞台;关闭回落原位(<kbd>Esc</kbd> / 背景点击 / 关闭钮)</li>
<li><strong>chrome 双层淡入</strong>:开箱后层 1(箭头/计数)先入、层 2(caption/EXIF)随入;<kbd>i</kbd> 隐藏全部;放大后自动隐层</li>
<li><strong>连拍导航</strong>:<kbd>←</kbd> <kbd>→</kbd> / 边缘箭头 / 触屏快滑;计数 n / N</li>
<li><strong>缩放</strong>:滚轮锚点缩放 1–4x、<kbd>双击</kbd> 1x↔2.5x、放大后拖拽、双指缩放;右下角缩放指示</li>
</ul>
</div>
<div class="stage"><ul class="wall" id="wall"></ul></div>
</div>
<!-- 灯箱:fixed 覆盖层(生产等价搬回原生 <dialog>,focus-trap 由 dialog 免费提供) -->
<div class="lbx" id="lbx" hidden>
<div class="lbx-bg"></div>
<div class="lbx-win" id="win"><img class="lbx-pic" id="pic" alt=""></div>
<button class="lbx-btn lbx-close c1" id="bClose" aria-label="关闭(Esc)">
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="m5 5 14 14M19 5 5 19"/></svg></button>
<button class="lbx-btn lbx-prev c1" id="bPrev" aria-label="上一张(←)">
<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m14.5 5.5-6.5 6.5 6.5 6.5"/></svg></button>
<button class="lbx-btn lbx-next c1" id="bNext" aria-label="下一张(→)">
<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m9.5 5.5 6.5 6.5-6.5 6.5"/></svg></button>
<div class="lbx-count c1" id="count">1 / 5</div>
<div class="lbx-meta c2">
<p class="lbx-caption" id="cap"></p>
<div class="lbx-row"><strong class="lbx-title" id="ttl"></strong><span class="lbx-exif" id="exif"></span>
<span class="lbx-acts"><a class="lbx-act" id="aOrig" href="#" target="_blank" rel="noopener">原图 ↗</a><a class="lbx-act" id="aDl" href="#" download>下载 ↓</a></span></div>
</div>
<div class="lbx-hint" id="hint">滚轮缩放 · 双击放大 · Esc 关闭 · i 隐藏界面</div>
<div class="lbx-zind" id="zind">100%</div>
</div>
<script>
/* ===== 数据:仓内实拍(含 2.36:1 长横幅与 2:3 竖图两种 letterbox 形态) ===== */
const PHOTOS=[
{src:'../content/photos/1766156983093.jpg',w:2400,h:1600,sp:13,title:'防波堤暮色',
cap:'落日把海与城一齐烫成金箔,海鸟的剪影恰好掠过日轮',exif:'X-T5 · XF35mmF1.4 · f/5.6 · 1/500s · ISO 200'},
{src:'../content/photos/mmexport1771816382115.jpg',w:2400,h:1018,sp:6,title:'长滩低云',
cap:'涨潮前的长滩,云层压得很低',exif:'X100VI · f/8 · 1/1000s · ISO 160'},
{src:'../content/photos/1766156998344.jpg',w:1600,h:2400,sp:20,title:'旗与塔',
cap:'旗在风里翻卷,塔身亮得像一支随时点火的火箭',exif:'GR III · f/2.8 · 1/250s · ISO 400'},
{src:'../content/photos/1766157004116.jpg',w:2400,h:1536,sp:9,title:'暮色撤离',
cap:'灯柱次第亮起,暮色像退潮一样撤离',exif:'X-T5 · XF56mmF1.2 · f/1.4 · 1/120s · ISO 800'},
{src:'../content/photos/mmexport1776564092433.jpg',w:2400,h:1600,sp:13,title:'夜航',
cap:'货轮的灯在雾里连成一条迟疑的线',exif:'X-T5 · XF70-300mm · f/4 · 1/60s · ISO 1250'}];
const N=PHOTOS.length;
const $=id=>document.getElementById(id);
const lbx=$('lbx'),win=$('win'),pic=$('pic'),wall=$('wall');
const reduced=matchMedia('(prefers-reduced-motion: reduce)').matches;
let idx=0,open_=false,hintShown=false;
let zoom={s:1,tx:0,ty:0};
/* ===== 影像墙 ===== */
PHOTOS.forEach((p,i)=>{
const li=document.createElement('li');
li.className='shot'; li.style.setProperty('--sp',p.sp);
const b=document.createElement('button'); b.type='button'; b.setAttribute('aria-label',p.title);
b.dataset.i=i;
const im=document.createElement('img'); im.src=p.src; im.alt=p.title; im.loading=i<3?'eager':'lazy';
b.appendChild(im); li.appendChild(b); wall.appendChild(li);
b.addEventListener('click',()=>openLbx(i));
});
const thumbImg=i=>wall.querySelector(`button[data-i="${i}"] img`);
/* ===== 几何:舞台 fit 矩形 / cover-contain 双盒(PhotoSwipe data-cropped 路线) ===== */
function stageBox(){const W=innerWidth,H=innerHeight;
const side=W>760?56:12,top=64,bot=innerWidth>560?124:150;
return{x:side,y:top,w:W-side*2,h:H-top-bot};}
function fitRect(p,box){const r=p.w/p.h,br=box.w/box.h;let w,h;
if(r>br){w=box.w;h=w/r;}else{h=box.h;w=h*r;}
return{x:box.x+(box.w-w)/2,y:box.y+(box.h-h)/2,w,h};}
function coverIn(p,winR){const r=p.w/p.h,wr=winR.w/winR.h;let w,h;
if(wr>r){h=winR.h;w=h*r;}else{w=winR.w;h=w/r;}
return{x:(winR.w-w)/2,y:(winR.h-h)/2,w,h};} /* 相对 win 的坐标 */
const setBox=(el,g)=>{el.style.left=g.x+'px';el.style.top=g.y+'px';
el.style.width=g.w+'px';el.style.height=g.h+'px';};
/* ===== 开箱:从缩略图原位起飞 ===== */
function fillMeta(p){$('cap').textContent=p.cap;$('ttl').textContent=p.title;
$('exif').textContent=p.exif;$('count').textContent=`${idx+1} / ${N}`;
$('aOrig').href=p.src;$('aDl').href=p.src;$('aDl').setAttribute('download',p.title+'.jpg');}
function openLbx(i){
idx=i;const p=PHOTOS[idx];pic.src=p.src;pic.alt=p.title;fillMeta(p);
const t=thumbImg(i).getBoundingClientRect();
const tr=fitRect(p,stageBox());
zoom={s:1,tx:0,ty:0};pic.style.transform='none';pic.classList.remove('zoomed','grabbing','swap');
lbx.hidden=false;open_=true;document.body.classList.add('locked');
lbx.classList.add('landing');
setBox(win,t);setBox(pic,coverIn(p,t)); /* 起点:与缩略图逐像素同框(cover 裁切) */
if(reduced){lbx.classList.add('on');lbx.classList.remove('landing');setBox(win,tr);setBox(pic,{x:0,y:0,w:tr.w,h:tr.h});}
else{
void win.offsetWidth; /* 强制 reflow,起点先落定 */
lbx.classList.add('on');
setBox(win,tr);setBox(pic,{x:0,y:0,w:tr.w,h:tr.h}); /* 终点:舞台 contain */
setTimeout(()=>lbx.classList.remove('landing'),330);
}
if(!hintShown){hintShown=true;const h=$('hint');h.classList.add('show');
setTimeout(()=>h.classList.remove('show'),2600);}
}
/* ===== 关闭:回落到缩略图原位 ===== */
function closeLbx(){
if(!open_)return;
const p=PHOTOS[idx],t=thumbImg(idx)?.getBoundingClientRect();
instantResetZoom();
if(reduced||!t){lbx.hidden=true;lbx.classList.remove('on','clean','noui');open_=false;
document.body.classList.remove('locked');return;}
lbx.classList.add('landing');
setBox(win,t);setBox(pic,coverIn(p,t));
lbx.classList.remove('on','clean','noui');
setTimeout(()=>{lbx.hidden=true;lbx.classList.remove('landing');open_=false;
document.body.classList.remove('locked');},320);
}
/* ===== 换片:旧图淡出、新图自侧向 10% 淡入(spacing 节奏) ===== */
function show(i,dir){
idx=((i%N)+N)%N;const p=PHOTOS[idx];instantResetZoom();fillMeta(p);
pic.src=p.src;pic.alt=p.title;
const tr=fitRect(p,stageBox());setBox(win,tr);setBox(pic,{x:0,y:0,w:tr.w,h:tr.h});
if(!reduced){pic.classList.add('swap');
pic.style.transform=`translateX(${dir*-10}%)`;
void pic.offsetWidth;pic.classList.remove('swap');
pic.style.transform='none';}
}
/* ===== 缩放:滚轮锚点 / 双击 / 双指 / 拖拽(复用线上 v2 数学) ===== */
function clampPan(){const mx=Math.max(0,pic.clientWidth*(zoom.s-1)/2),
my=Math.max(0,pic.clientHeight*(zoom.s-1)/2);
zoom.tx=Math.min(mx,Math.max(-mx,zoom.tx));zoom.ty=Math.min(my,Math.max(-my,zoom.ty));}
function applyZoom(){clampPan();
pic.style.transform=zoom.s===1?'none':`translate(${zoom.tx}px,${zoom.ty}px) scale(${zoom.s})`;
pic.classList.toggle('zoomed',zoom.s!==1);
lbx.classList.toggle('clean',zoom.s!==1);}
function instantResetZoom(){const keep=pic.style.transition;pic.style.transition='none';
zoom={s:1,tx:0,ty:0};pic.style.transform='none';pic.classList.remove('zoomed');
lbx.classList.remove('clean');void pic.offsetWidth;pic.style.transition=keep||'';}
function zoomAt(next,cx,cy){const r=pic.getBoundingClientRect();
const ax=cx-r.left-r.width/2-zoom.tx,ay=cy-r.top-r.height/2-zoom.ty;
if(next<=1){zoom={s:1,tx:0,ty:0};applyZoom();zindShow();return;}const k=next/zoom.s;zoom.tx-=ax*(k-1);zoom.ty-=ay*(k-1);zoom.s=next;applyZoom();zindShow();}
let zt;function zindShow(){const z=$('zind');z.textContent=Math.round(zoom.s*100)+'%';
z.classList.add('show');clearTimeout(zt);zt=setTimeout(()=>z.classList.remove('show'),900);}
lbx.addEventListener('wheel',e=>{if(!open_)return;e.preventDefault();
const f=Math.exp(-e.deltaY*0.0015);
const next=Math.min(4,Math.max(1,zoom.s*f));
if(next!==zoom.s)zoomAt(next,e.clientX,e.clientY);},{passive:false});
win.addEventListener('dblclick',e=>{if(zoom.s===1)zoomAt(2.5,e.clientX,e.clientY);
else instantResetZoom();}); /* 挂 win:放大态 setPointerCapture 会把 click 派发给 win,pic 收不到 */
/* 拖拽平移(放大态)+ 双指缩放:pointer 事件统一 */
const pts=new Map();let drag=null,pinch=null;
win.addEventListener('pointerdown',e=>{
pts.set(e.pointerId,{x:e.clientX,y:e.clientY});
if(pts.size===2){const a=[...pts.values()];
pinch={d0:Math.hypot(a[0].x-a[1].x,a[0].y-a[1].y),s0:zoom.s,
mx:(a[0].x+a[1].x)/2,my:(a[0].y+a[1].y)/2};drag=null;return;}
if(zoom.s>1){win.setPointerCapture(e.pointerId);
drag={x:e.clientX,y:e.clientY,tx:zoom.tx,ty:zoom.ty};pic.classList.add('grabbing');}
});
win.addEventListener('pointermove',e=>{
if(pts.has(e.pointerId))pts.set(e.pointerId,{x:e.clientX,y:e.clientY});
if(pinch&&pts.size===2){const a=[...pts.values()];
const d=Math.hypot(a[0].x-a[1].x,a[0].y-a[1].y);
const next=Math.min(4,Math.max(1,pinch.s0*d/pinch.d0));
if(next!==zoom.s)zoomAt(next,pinch.mx,pinch.my);return;}
if(drag){zoom.tx=drag.tx+e.clientX-drag.x;zoom.ty=drag.ty+e.clientY-drag.y;applyZoom();}
});
const endPt=e=>{pts.delete(e.pointerId);if(pts.size<2)pinch=null;
if(drag){drag=null;pic.classList.remove('grabbing');}};
win.addEventListener('pointerup',endPt);win.addEventListener('pointercancel',endPt);
/* 触屏快滑翻页(zoom=1 时) */
let x0=null;lbx.addEventListener('touchstart',e=>{x0=e.touches[0].clientX;},{passive:true});
lbx.addEventListener('touchend',e=>{if(x0===null||zoom.s!==1){x0=null;return;}
const dx=e.changedTouches[0].clientX-x0;
if(Math.abs(dx)>40)show(idx+(dx<0?1:-1),dx<0?1:-1);x0=null;},{passive:true});
/* ===== 键盘 / 背景点击 / chrome 召唤 ===== */
document.addEventListener('keydown',e=>{
if(!open_)return;
if(e.key==='Escape')closeLbx();
else if(e.key==='ArrowLeft')show(idx-1,-1);
else if(e.key==='ArrowRight')show(idx+1,1);
else if(e.key==='i'||e.key==='I')lbx.classList.toggle('noui');
});
lbx.addEventListener('click',e=>{
if(e.target.closest('.lbx-win')||e.target.closest('.lbx-btn')||
e.target.closest('.lbx-meta')||e.target.closest('a'))return;
closeLbx(); /* 背景点击关闭(bgClickAction:'close' 同款) */
});
$('bClose').addEventListener('click',closeLbx);
$('bPrev').addEventListener('click',()=>show(idx-1,-1));
$('bNext').addEventListener('click',()=>show(idx+1,1));
/* 开箱中窗口变化:舞台即时重排(不做动画,避免与缩放打架) */
addEventListener('resize',()=>{if(!open_||lbx.classList.contains('landing'))return;
const p=PHOTOS[idx];const tr=fitRect(p,stageBox());
setBox(win,tr);setBox(pic,{x:0,y:0,w:tr.w,h:tr.h});});
</script>
</body>
</html>
@@ -0,0 +1,125 @@
# 博客体验研究与优化方案调研(六站)
> 任务产出。数据来源:2026-10-26 直连抓取六站 HTML(文本提取后人工分析)。抓取失败的站已如实标注。
> 站点快照存于 /tmp/uxstudy/*.html(临时,会话级)。目标站背景:Astro SSG 双语、灰阶极客风、命令面板/键盘弦/终端彩蛋/热力图/PWA/Artalk、自建 gitea+Sveltia 照片管线。
## 一、六站逐站笔记
### 1. qh1009.net(清凉药)✅ 已抓取(66KB,WordPress + WiThemes)
- **首页 IA**:纯内容流——俳句/诗全文直接铺在首页(非标题列表),编号制(#219…#2008),每条极短 + 标签。首页即作品。
- **排版**:文学向极简,正文居中窄栏,无目录/无进度条/无评论组件可见(0 评论数展示)。
- **特色交互**:几乎零交互,靠"编号+日期+分类词(俳/诗/译/博物)"的仪式感取胜。
- **导航/移动端**:顶部 Toggle Menu,Home/关于/作品/杂记/博物/俳/诗/译/Podcast,移动折叠。
- **内容组织**:标签云非常细(50+ 自然意象词),年归档分页。
- **管理**:WordPress(页脚 "Designed by WiThemes"),推断经典 WP 后台。
- **可借鉴**:瞬间(moments)可学"编号制"给每条瞬间一个序号,制造连续感;标签的"意象化命名"比功能化命名更有味道。
### 2. nbfox.com(麦田艺术)✅ 已抓取(187KB,WordPress 画廊站)
- **首页 IA**:题材/流派双维下拉菜单(风景画/肖像画… × 印象派/巴洛克…),首屏专题卡片(手机壁纸/梵高向日葵/浮岳三十六景)+ 大图瀑布。
- **排版**:图为主体,每图带结构化元数据卡:创作者/年代/图片原始尺寸(像素)/风格——元数据即说明文字。
- **特色**:①双维分类导航(题材×流派)②图片标注精确像素尺寸,管理下载预期 ③"超高清/热门收藏/精选"策展位。
- **导航/移动端**:粘性双搜索框 + 汉堡菜单,功能/会员/收藏夹分离。
- **内容组织**:博物馆聚合、公域版权资源库、网盘分发——内容型产品打法。
- **管理**:WordPress + WooCommerce(会员/网盘),重后台。
- **可借鉴**:**图库(gallery)加"双维筛选"**(地点×器材 或 主题×年份);每张照片展示 EXIF 元数据卡(机型/焦段/光圈/地点)——我们已有 exifr 管线,只差展示层。
### 3. yongyuan.name ✅ 已抓取(56KB,静态技术博客)
- **首页 IA**:一句个人格言 + "All Posts" 按年份倒序全量清单(2015→2024,一页全列),每条:日期/标题/分类。
- **排版**:无摘要、无图,纯列表——十年技术文一目了然,靠"全量可见"建立信任。
- **特色交互**:无。反例价值:极简到牺牲发现性(无搜索/无热门/无相关文)。
- **内容组织**:分类词前置在每行(CV/推荐系统/C++/ANN),每年末一篇"年终总结"锚点文。
- **可借鉴**:**归档页全量清单模式**(我们有 archives/,确认是否一页可览十年);"年终总结"惯例值得立为个人传统。
### 4. ruanyifeng.com/blog ✅ 已抓取(首页 20KB + 周刊分类页 176KB,自建 MT 系统)
- **首页 IA**:最新一篇全文摘要 + "继续阅读全文 »" + 右栏:最新文章 5 条 / 最新留言 / 关于 / 邮件订阅 / 授权声明 / 社交账号——**首页即门户**,信息密度极高但层级清晰。
- **文章列表**:周刊独立分类页,416 篇按年分组倒序,纯标题+日期列表。首页摘要带"分类: 周刊"标签 + 留言数(30)——**留言数前置是社交证明**。
- **内容组织(标杆点)**:①周刊作为**固定节奏产品**(周五发布,416 期,编号化)②完整档案页(分类/按月/2004 至今)③邮件订阅+RSS 双通道 ④留言 79533 条展示在侧栏(社区感)⑤版权/授权声明独立成块。
- **排版详情**:正文窄栏衬线、目录在长教程文中手写小节链接、代码块朴素无高亮(极客反潮流)。
- **可借鉴**:①**固定栏目化**——我们的"瞬间"可升格为编号制轻周刊(每周 3-5 条瞬间聚合 + 一句话评);②侧栏"最新留言"(Artalk API 可拉);③留言数展示在列表页。
### 5. antfu.me ✅ 已抓取(首页/列表页/文章页,Astro + UnoCSS,同栈)
- **首页 IA**:一段自我介绍正文 + "Recent work" 项目动态(直接拉 GitHub 数据)+ 一图流头部。无文章列表——博客藏在 /posts。
- **文章列表**:按年份分组,每条:日期 · 阅读时长("Sep 16 · 15min")· 语言徽标(中文/日本語)——**双语不是双站,是同一列表内并列两行互链**("Hello Tokyo!" / 「おはよう、東京!」相邻)。
- **排版/交互(值得抄)**:①`slide-enter-content` 入场动画(我们已有同类)②magic-link:GitHub 链接自动渲染成卡片、图片自动预加载(grep 实锤 markdown-magic-link 类)③单篇文章页有 "Demo of …" 双语切换按钮 + **Plaintext 纯文本模式**(去样式阅读)④暗色以 class 切换,全站色调极克制(text-gray-700/200)。
- **导航**:极简 5 项(Blog/Projects/Talks/Sponsors),移动端同构。
- **管理**:纯 git 仓 + 自己写的构建,无 CMS——证明"无后台"路线在顶级生产者处成立。
- **可借鉴**:①**双语互链并列**(比我们 /en/ 双轨更轻,可作文章页"读中文版"强化);②**阅读时长估算**列表页展示;③**纯文本/阅读模式**(灰阶站天然适配,一行 toggle + CSS);④首页 "Recent work" 拉 gitea commit 流(我们已有"最近心跳",可扩展为开源项目动态)。
### 6. innei.in ✅ 已抓取(141KB,Mix Space + Shiro 主题,React SSR)——中文现代个人站代表
- **首页 IA**:问候 + 一言(随机引言)+ 统计条(X 篇 · X 万字 · X 天)+ "近期笔墨"5 条(类型徽标:笔记/文章/技术)+ 页脚多宫格(关于/照片廊/联系/订阅/版式开关)。
- **特色(值得抄)**:①**站内搜索/主题/背景音乐/背景效果/余白开关全在设置面板**("版式"区:浅色·随系统·深色 / 背景音乐 关·开 / 背景效果 关·开)——把实验性开关收纳成一个面板;②多内容类型(文稿/手记/时光/思考)+ 订阅通信(Newsletter 入口);③留印记(留言板)、朋友们(友链)、一言。
- **导航**:顶部 4 项 + "更多",移动端宫格化页脚导航。
- **管理**:Mix Space 自带 admin 后台(草稿/评论/监控),有 API——对比样本。
- **可借鉴**:①**设置面板收纳实验开关**(我们的终端彩蛋/辉光/动效可聚成一个"偏好"面板并 localStorage 持久化——比散落各处更克制);②Newsletter 订阅入口;③友链页。
### 抓取情况汇总
六站全部抓取成功(qh1009/nbfox/yongyuan/ruanyifeng/antfu/innei),无备选站需补。注:本机代理(7897) TLS 握手故障,全部改直连抓取;innei.in 首屏计数显示 0(数据懒加载),已从 DOM 结构推断功能。
## 二、共性模式清单(优质站普遍做、我们没做/未确认)
| # | 是什么 | 价值 | 灰阶极客风适配建议 |
|---|---|---|---|
| 1 | **阅读时长估算**(antfu "15min") | 列表页决策成本↓ | PostList 每条加 `~8 min` 等宽字体角标,构建期按字数/分钟计算,零运行时 |
| 2 | **双语条目互链并列**(antfu 同列表 zh/en 相邻互指) | 双语发现性↑,用户不会漏另一语版 | 文章页标题下加一行 `→ EN` 静态链接即可(已有 /en/ 路由,缺的是入口可见性) |
| 3 | **纯文本/阅读模式**(antfu Plaintext) | 极客好评、灰阶天然契合 | 单条 CSS class 剥离装饰(辉光/动效/侧栏),命令面板加 `reader` 命令 + 快捷键 |
| 4 | **留言数/评论数前置到列表**(阮一峰"留言(30)") | 社交证明,激活 Artalk 存量数据 | Artalk API 构建期拉取各页评论数,写进 frontmatter 化的 JSON,列表页静态渲染 |
| 5 | **侧栏/首页"最新留言"**(阮一峰) | 站点"活着"的信号 | 首页"最近心跳"旁并列"最新评论"一条流(构建期快照,接受 24h 陈旧) |
| 6 | **固定节奏栏目化**(阮一峰周刊 416 期编号) | 读者形成期待,更新压力可控 | "瞬间"页顶加"本周瞬间 #N"聚合块,编号来自构建期统计 |
| 7 | **图片元数据卡**(nbfox 尺寸/创作者/年代) | 摄影轨的专业感 | gallery 详情展示 exifr 已提取的 EXIF(机型/焦段/光圈/ISO/时间/地点)等宽字体表 |
| 8 | **归档全量清单**(yongyuan 十年一页) | 纵深感与可扫读 | 确认 archives/ 是全量单页;不够则按年分组渲染 |
| 9 | **偏好/实验开关集中面板**(innei 版式区) | 克制与自由兼得,避免装饰散弹 | 命令面板加 `settings` 命令:辉光/动效/热力图/背景音乐各自 toggle,localStorage 持久化 |
| 10 | **Newsletter 订阅入口**(innei/antfu sponsor+订阅、阮邮件订阅) | 回访通道不被算法绑架 | P2→P1 观望:无邮件服务前先放 RSS 说明页强化;接入自托管 listmonk 是远期项 |
| 11 | **友链页**(innei 朋友们) | 个人站社区礼节,互推 | /about 或独立 /links,静态 YAML 驱动 |
| 12 | **首页项目动态**(antfu Recent work 拉 GitHub) | 开源轨的活性证明 | "最近心跳"已含 commit 脉冲,可加"开源动态"一列(gitea API,构建期) |
## 三、差异化机会(我们独有,保持强化)
1. **命令面板 Ctrl+K + vim 键位弦(g g / g s 双击 chord)**——六站无一做到;持续补齐 chord E2E 矩阵即可,是护城河。
2. **终端彩蛋页 + 页脚 commit hash**——antfu 级极客也只做了 slide 动画;我们的终端隐喻是身份标识,新功能优先以"命令"形态暴露(如 reader、settings 走命令面板而非按钮)。
3. **构建期零 JS 摄影 masonry(exifr+sharp)**——innei 照片廊是 React 重运行时;我们的构建期管线是自托管价值观的最佳注脚,配上 EXIF 元数据卡(共性#7)即成完整叙事。
4. **构建期 LLM 预翻译 + .en.md 入仓 + AI 标注**——antfu 双语靠手写;我们的管线差异化在"过程可审计"(入仓),强化 AI 译徽标的诚实性。
5. **PWA 离线全量 + 安装 CTA 三态**——六站均无 PWA;已有,勿再投入,保持零回归即可。
6. **灰阶克制本身**——阮一峰证明了"朴素不等于廉价";我们与 antfu 的 gray-700/200 同谱系,方向正确。
## 四、管理后台便利性专题
### Sveltia vs 主流后台(WordPress/Typecho/Halo)差距面
| 能力 | WP/Halo/Typecho | Sveltia(git 仓) | 补齐方案 |
|---|---|---|---|
| 草稿预览 | 即时预览 | 编辑侧预览,但布局/组件 ≠ 成品 | **/draft/ 构建路由**:draft frontmatter 的文章构建进 `/draft/<slug>/`,nginx 层加 BasicAuth 或长随机路径;发布=移除 draft 标志 |
| 媒体库 | 中央媒体库、检索、批量 | 文件夹浏览、上传即入 git | 我们照片走 gitea web 上传已等价;补 **构建期图片 lint**(exifr 校验尺寸/格式/命名规范,违规即构建警告清单页) |
| 定时发布 | 原生 | 无 | **构建期 publishDate 判定**:CI 触发频率内生效(webhook+定时 cron 双触发 gitea actions / receiver),文章带未来日期默认不出现在正式列表 |
| 批量操作 | 后台多选 | git 原生(批量改文件+一次 commit) | 无需补;可在 scripts/ 加批量 frontmatter 工具(改日期/标签) |
| 版本历史 | 修订版 | git 全量历史 | 已胜出;页脚 commit hash 已把它可视化了 |
### "自托管+git 仓"架构补齐清单(按性价比排序)
1. `/draft/` 预览路由(上面方案)——写作体验最大缺口。
2. 构建期内容 lint:断链检查、图片存在性、frontmatter schema(typebox 已在栈内)、双语文件配对校验(zh 无 en 时警告)。
3. 构建期质量报告页(仅 draft 可见):字数/阅读时长/图片体积/未翻译词条清单,写作完成度仪表。
4. Sveltia config.yml 优化:为 moments/gallery 集合配 i18n 双语字段映射、媒体文件夹预排序(按日期倒序,避免长列表翻找)。
5. 发布检查单自动化:构建尾跑 LH 冒烟(gz-server.py 已在仓)+ 死链扫描,fail 即 webhook 通知 gitea。
## 五、分级建议表
| 级 | 项 | 理由 |
|---|---|---|
| **P0** | 阅读时长角标(#1) | 一次构建期计算,全站收益 |
| **P0** | 文章页"→ EN/中文版"互链(#2) | 双语投入的回报被入口缺失吃掉 |
| **P0** | /draft/ 预览路由(四-1) | 写作流最大缺口,git 架构易实现 |
| **P0** | 构建期内容 lint(四-2) | 防线前移,typebox 已在栈内 |
| **P1** | 命令面板 reader 纯文本模式(#3) | 极客好评且实现轻,走命令面板零 UI 负担 |
| **P1** | 偏好面板收纳实验开关(#9) | 辉光/动效已有但散落,收纳更克制 |
| **P1** | gallery EXIF 元数据卡(#7) | exifr 数据已在手,纯展示层 |
| **P1** | 列表评论数 + 首页最新评论流(#4/#5) | 激活 Artalk 存量;接受构建期快照陈旧 |
| **P1** | 瞬间编号化"本周瞬间 #N"(#6) | 低成本仪式感 |
| **P1** | 归档全量清单复核(#8) | 只需确认,可能已达标 |
| **P1** | 友链页(#11) | 静态 YAML,一晚工作量 |
| **P2** | Newsletter/邮件订阅(#10) | 需自托管邮件栈(listmonk+发信域名),维护成本先于价值;先强化 RSS |
| **P2** | 背景音乐/一言/背景特效(innei 同款) | 与灰阶克制冲突:多感官装饰稀释"终端"隐喻的一致性 |
| **P2** | 首页大图专题卡、瀑布流重化(nbfox 同款视觉) | nbfox 是内容电商型画廊;我们零 JS masonry 已是更优解,加重视觉与"灰阶骨架×分轨正文"定稿相悖 |
| **P2** | 阅读进度条/浮动 TOC 大改 | 六站中仅少数有 TOC;我们长文少,现有目录足够,勿为趋势加常驻 UI |
## 风险与备注
- antfu.me 文章页部分组件(Plaintext 按钮)为客户端渲染,快照中由类名/结构推断,实现前建议人工核对 antfu 仓(antfu.me 开源)。
- innei.in 首屏计数 0 为懒加载,统计功能存在但数值未取到。
- 评论数/最新评论若走 Artalk 需注意 ESA 缓存(此前已有 /artalk/api 绕缓存议题),构建期拉取可绕开运行时问题。
+146
View File
@@ -0,0 +1,146 @@
# 博客优化方案 v2(深度版)
> 最终落位:`docs/research/07-博客优化方案v2.md`(本文件为 scout 产出原稿,内容即完整方案)。
> 数据来源:2026-10-26 12:05-12:30 生产实测(curl 直抓 wait4x.online + 本机 nginx/Artalk 容器 + 仓库源码)与境外站直连抓取(本机 clash 7897 节点故障,`--noproxy '*'` 直连成功,复现 v1 教训)。抓取失败项已如实标注。
> 前置:v1 六站研究见 docs/research/06;本项目历史调研 01-05。
## 1. 执行摘要(推荐立即做的 8 件事)
1. **解除全站 noindex 封印**(0.5d):nginx 共享 snippet 给博客 vhost 注入 `X-Robots-Tag: noindex`,全站被标记不可索引——**这是生产 SEO 60-63 的第一主因,所有内容工作在这之前等于闭门写**。
2. **补 robots.txt + ESA 全站 purge 一次**(0.5d):public/ 从未有 robots.txt(生产 404);noindex 响应头已被 ESA 边缘缓存 30 天,修完必须 purge。
3. **修 localePath 双 en bug + 加自指 canonical**(0.5d):en 页 hreflang 自指 `/en/en/archives/…`(404);同页带/不带尾斜杠双 200。
4. **构建期内容 lint**(1d):断链/图片存在性/frontmatter schema/双语配对,build.sh 守卫挂钩(防线前移,v1 P0 坚持项)。
5. **/draft/ 预览路由**(1d):写作流最大缺口;长随机路径 + robots noindex + 不入 sitemap。
6. **reader 纯文本模式**(0.5d):`html.reader` 单 class 方案,落点已全部排明(§4.4);命令面板 `reader` 命令 + `r` 键。
7. **列表页评论数前置**(1d):Artalk API 已实测(`GET /api/v2/comments?…&site_name=wait4x.online` 返回 `count`),构建期拉取静态渲染,零运行时。
8. **首页 title/description 语义化**(0.5d):现全站共用 `wait4x` + 一句通用 description,列表/文章页 title 已对,home 与 moments/gallery 等仍弱。
## 2. v1 P0/P1 批判性复审
| v1 项 | v1 级 | 复审结论 | 理由 |
|---|---|---|---|
| 阅读时长角标 | P0 | **降 P1**:文章页已有(`[...slug].astro:39` 汉字数/400 取整,`:60` 已渲染"阅读时长 N 分钟");v1 说的其实是列表页角标,页内已有 → 列表是锦上添花 |
| →EN 互链 | P0 | **关闭**:Header `nav-lang` EN 链接已每页常驻(车道 G 出舱后任何宽度可见)。但复查揪出真 bug:en 页 hreflang 双 en(见 §4.1-3)——**从"加互链"转化为"修 localePath"** |
| /draft/ 预览路由 | P0 | **坚持**:git 架构最大写作缺口。实现降级:长随机路径优先于 BasicAuth(nginx 现无 auth 依赖,别为 draft 引入) |
| 构建期 lint | P0 | **坚持并升级**:SEO 三连漏检证明"只在本地测"会漏生产层故障——lint 应加"生产冒烟"(curl robots/sitemap/头)一档 |
| reader 纯文本 | P1 | **坚持升 P0 批**(与 SEO 同批做):antfu 同款 + 灰阶天然适配,本轮落点清单已备好,半天可收 |
| 偏好面板 | P1 | **降级收窄**:先只做 reader + 辉光/动效开关两个 toggle,防"设置中心"过度设计(innei 面板是 React 重站的路数) |
| gallery EXIF 卡 | P1 | **坚持**:photos_ingest.py 构建期 exifr 数据已在手,纯展示层 |
| 评论数前置+最新评论 | P1 | **坚持,风险清零**:v1 担心的"API 形状/ESA 缓存"本轮实测解决——构建期从 127.0.0.1:13030 内网直拉(构建机=Artalk 宿主),完全不碰 ESA |
| 瞬间编号化 | P1 | **保留 P1 尾部**:仪式感成立(diygod `#年终总结` 6 篇、阮一峰 416 期都验证了节奏感),但数据量小(21 图+1 瞬间),收益后置 |
| 归档全量复核 | P1 | **关闭(已达标)**:实测 /archives/ 全量单页,zh 6 条 + en 6 条(7 个 md 减 media-sample 的 hidden 标记),无分页,一页可扫 |
| 友链页 | P1 | **保留**:diygod `/friends` 友邻页再次验证(中文个人站礼节),YAML 驱动一晚 |
| Newsletter | P2 | **维持观望**:xeiaso 用 Patreon/signalboost 但那是有 400 文+社群的体量;我们 RSS 刚起步,先 RSS 说明页 |
**v1 最大盲区(自我批判)**:v1 调研全部基于"站点代码应该没问题"的假设,只测了视觉/交互,没测生产响应头与爬虫视角——本轮 SEO 60-63 的三个根因(noindex 头、robots 缺失、hreflang 404)全部存在于 v1 调研期间,一个都没被 v1 发现。教训:**体验研究必须包含"爬虫/机器人视角"的生产实测**,已补进 §4。
## 3. 新探索站增量发现(v2 补面,8 站)
> 本机 clash 7897 节点当日故障(google/github 经代理均 000),全部改 `--noproxy '*'` 直连成功——与 v1"代理 TLS 故障改直连"同款环境病,抓境外站先直连再代理。
1. **rauno.me**(9.7KB 瘦 HTML,实测)——交互工艺标杆,但真正的标杆在子页:`/craft` 是**按月编年的工艺案例库**(2020→2026,30+ 条:Toolbar Morph/X-Ray Interaction/⌘K Breadcrumbs…,每条三种出口 View Production/View Prototype/Read Essay)。首页即一句信条循环("Make it fast. Make it beautiful… Make it.")+ **Email 点击即复制**("Copied" 内联反馈)+ **自我版本化**(2023/2022 年度快照链接)。可借鉴:①把 terminal 页或独立页做成"工艺/极客案例编年体";②页脚 email 用复制微交互替代 mailto;③站内 About 写"信条列表"比自我介绍段更有身份感。
2. **diygod.cc**(RSSHub 作者站,实测)——中文极客个人站代表:首页格言 + **分类计数前置**(创作集26/分享境18/研习坊16/日记本13——中文意象化命名,与 v1 qh1009 结论互证);/posts **90 篇前端关键词搜索**(零后端);**双语 /en 切换**;`#年终总结` 6 篇的节奏传统。**彩蛋级发现:页面内嵌一段给 AI 爬虫的 persona prompt**("你是一个擅于模仿真实人类写博客的 AI,名字叫 DIYgod…")——对 AI 爬虫的幽默防御/人格注入,我们 terminal 页可同款玩。RSS 姿态:作者即 RSSHub 之父,站内 RSS 极简入口 + 完整摘要输出(15/15 全文)。
3. **brianlovin.com**——**抓取失败(如实标注)**:Vercel Security Checkpoint JS 挑战挡 curl(HTTP 429),本机 c4ai 无头浏览器因代理故障不可用。其"写作/项目/梯次"结构引用 v1 常识不采信,标注为待人工复核。
4. **vincentwill.com**(实测,选它弃 sabrinacodes)——sabrinacodes 实抓后确认是 PHP 教师 portfolio(Resume/Education/Mini Games),与我们三轨内容零交集,弃;vincentwill 为柏林 web 开发者:**微产品墙一行一产品**(名+一句话+链接,[Sold] 标记),"latest projects" 精选 6 + "More Projects" 全量折叠——与开源项目墙"双源"同构,可借鉴其**精选/全量两档**而非平铺。
5. **xeiaso.net**(实测)——400+ 文的终端极客活化石:blog 列表页顶部**RSS+JSONFeed 双提示语**("If you have a compatible reader…");**系列文仪式**("No way to prevent this" 同标题连发=美国枪击梗的技术圈复刻,系列性本身就是品牌);页脚**最新 commit hash 链回 GitHub**(与我们 T7 同款,互证方向);多身份自我定义(technical educator/vtuber/philosopher)。可借鉴:①系列文(CS 配置/书评都可系列化命名);②blog 顶部 RSS 友好提示语;③signalboost(转发求职/求助信息)是社群位远期选项。
6. **shud.in**(自选①,理由:中文 design engineer、Vercel 核心贡献者的极简样本,与"灰阶骨架"同谱系)——实测四页 IA(About/Thoughts/Projects/Guestbook),首页一段自我叙述+社交链接,零装饰零动效。**Guestbook 留言板**=轻量社区位(Artalk 现成能力);"Thoughts"命名比"博客"更有随笔感。启示:**我们的极简已达标,不需再减;缺的是 diygod/xeiaso 级的内容节奏与仪式**。
7. **Artalk 生态站**——**artalk.org 域名已易主**(实测标题"𝕜✺𝕫 𝕤𝕥𝕦𝕕𝕚𝕠 // healing arts therapy",Squarespace 疗愈站)⚠️;但官方实为 **artalk.js.org**,npm 包 `artalk` **2.10.1 于 2026-10-25(昨天)发布**,上游活跃。结论:我们自托管 artalk-go 2.10.0 容器不受域名易主影响;升级通道=Docker Hub;官方 demo 站本轮未访(js.org 子域未测),评论体验最佳实践改以"本机 2.10.0 API 实测"为准(§4.3)。**行动项:文档/书签里的 artalk.org 引用全部改 artalk.js.org**。
8. 增量共性(补 v1 十二条之外):①**分类/标签中文意象命名+计数前置**(diygod×qh1009 双验证)→ 我们 PostList 的 reading/tech/photo 三轨 zh 词条已是意象词,可加计数;②**系列文/编号传统**(xeiaso×diygod×阮一峰三方验证)→ "瞬间"编号化(v1 #6)证据更强;③**RSS 可见性**(diygod 全文输出+xeiaso 列表页提示语)→ 我们 rss.xml.ts 存在但入口只在命令面板+页脚,/articles/ 页顶可加一行订阅提示。
## 4. 自查实证(生产 × 仓库,全部本轮实测)
### 4.1 SEO 60-63 根因(三连,全实锤)
| # | 事实 | 证据 | 影响 |
|---|---|---|---|
| 1 | **全站 `X-Robots-Tag: noindex`** | `curl -I https://wait4x.online/` 与文章页均带该头;源=`/etc/nginx/snippets/esa-origin-ssl.conf:11`(`add_header X-Robots-Tag noindex always;`),博客 server 块(`sites-enabled/default:92` wait4x.online→18090)include 了这个给内部服务(api/oj/token/fc/latex/cpa)设计的 snippet | Lighthouse `is-crawlable` fail(重权重);**搜索引擎视角全站不可索引** |
| 2 | **robots.txt 不存在** | 生产 404(nginx);`public/` 与 `dist/` 均无 | Lighthouse robots 审计扣分;无 Sitemap 入口 |
| 3 | **en 页 hreflang 自指双 en** | 生产 HTML:`hreflang="en" href="…/en/en/archives/mian-sha/"`(实测 404);源=`src/lib/locale.ts` 的 `localePath('/en/…','en')` 未剥旧前缀,`BaseLayout.astro:91` 直接调用 | Lighthouse hreflang fail;每页必扣 |
次级问题:④无自指 canonical,且 `/en/archives/mian-sha`(Header nav-lang 的 href)与 `/en/archives/mian-sha/`(sitemap/hreflang 形状)**双 200 不重定向**(nginx try_files 直出),重复内容风险;⑤home `<title>wait4x</title>` 过薄;⑥**ESA 对 text/html 缓存 2592000s(30 天)**(`X-Swift-CacheTime`),当前靠 Last-Modified/ETag 回源重验证拿新内容(实测 Last-Modified=当日部署时刻),但响应头(含 noindex)固化在缓存对象里——**修复后不 purge 则一个月内爬虫仍见 noindex**。本地曾 100 的原因:测试床无 nginx snippet、无 ESA、robots.txt 404 在旧版 Lighthouse 不判 fail。
**修法(A 卡片)**:① snippet 剥离 noindex 行,6 个内部服务 vhost 各自 server 块补回(它们确实该 noindex),博客 vhost 不加;博客私有路径(/admin/、/draft/)单独 location 加 noindex;② `public/robots.txt`(Allow 全站,Disallow /admin/ /draft/,Sitemap 指向 sitemap-index.xml);③ localePath 改为先 normalize 到 zh 再拼目标前缀;④ BaseLayout 加自指 canonical(Astro.site + withSlash(rawPath)),Header nav-lang href 补尾斜杠;⑤ ESA 控制台对 wait4x.online 执行一次全站 purge,并评估 HTML 边缘 TTL 调至 ≤1h 或接 purge API(发布可见性保险)。
### 4.2 /archives/ 归档现状(v1 #8 复核)
实测:`/archives/` 全量单页,zh 6 条 en 6 条,年份分组+三轨分节(PostList.astro),无分页;`content/posts` 7 文,差 1 = media-sample(`hidden` tag,`archives/index.astro` 过滤条件 `!data.draft && !data.tags.includes('hidden')`,注释声明"视觉测试完成后应删除")。**结论:达标,零改动**;唯一尾单:视觉验收完成后删 media-sample(它仍占 sitemap 一条 URL)。
### 4.3 Artalk 评论数 API(列表页前置数据源,已实测)
- 形状:`GET /api/v2/comments?page_key=/archives/<slug>/&type=page&limit=0&site_name=wait4x.online` → `{"comments":[],"count":0,"roots_count":0,"page":{…,"pv":4,…}}`。**注意参数是 `site_name` 不是 `site`**(`site` 报 err_no_site)。匿名可读,无需管理员。
- site 值来源:`~/docker_apps/artalk/data/artalk.yml` `site_default: wait4x.online`;前端 ArtalkComments.astro 的 pv POST 已带 `site_name`,pageKey 口径一致(`/archives/<id>/`,中英共用不分裂)。
- **构建期直拉可行**:构建机=Artalk 宿主(同机 127.0.0.1:13030),绕开 ESA/nginx,零缓存语义问题;构建产物静态渲染数字,接受"构建时刻快照"陈旧(v1 已定此取舍)。
- 最新评论流(首页"最新心跳"旁):同端点 `type=all` 时仍要求 page_key,全站维度改用 `GET /api/v2/comments?type=all&site_name=…&limit=5` 需带任一 page_key 的坑已遇("Page_key必须填写")——**实施时以管理员 token 走 `/api/v2/pages`(需登录,`need_login:true` 实测)或退回逐页轮询 count 取 top-N**,两路都写在卡片验收里。
- 上游健康:artalk-go 2.10.0(在跑)→ npm artalk 2.10.1(2026-10-25 发);artalk.org 域名易主(§3-7),文档引用改 artalk.js.org。
### 4.4 reader 纯文本模式落点(html.reader 单 class 方案)
Body 结构(BaseLayout.astro):`.bg-glow`(背景辉光)→ `.skip-link` → `header.site-header.safe-area`(含 nav-actions 三钮)→ `main#main.container`(正文+`.toc`侧栏+`.pn`上下篇+`#artalk-container`)→ `footer.site-footer`。会被覆盖/剥离的选择器清单(global.css + 组件 scoped):
- 隐藏:`.bg-glow`、`.read-progress`(顶进度条)、Header `.nav-actions`(保留品牌+导航词)或整个 `.site-header` 收为一行、Footer 收为备案单行(ICP 陕ICP备2026014860号-1 **不可隐**——合规底线);
- 中性化:正文入场动画类(global.css 动效层 `prefers-reduced-motion` 已有的降级规则可复用同款写法)、列表微浮 hover、`.atk-pv-line`/`#artalk-container` 隐藏或保留(建议保留评论——reader 是去装饰不是去内容,antfu Plaintext 同款语义);
- 增强:`.prose-reading/.prose-tech` 字号微升一档、行宽略收(tokens.css 变量覆写)。
- 入口:CommandPalette `data-cmd="reader"`(照 `data-cmd="theme"` 模式,`src/components/CommandPalette.astro:42`)+ 键盘 `r`(Keyboard.astro 弦位表)+ localStorage 持久化;VT 软导航下 class 挂 `documentElement` 天然存活。
## 5. 实施方案卡片
**A|SEO 解封三件套 + ESA purge**(工作量 1d;风险:nginx 重载失误→保留 .bak 回滚,ESA purge 不可逆但无害)
- 目标:搜索引擎可索引,Lighthouse SEO ≥95。
- 做法:`/etc/nginx/snippets/esa-origin-ssl.conf` 删 noindex 行;`api/oj/token/fc/latex/cpa/git` 各 vhost server 块补 `add_header X-Robots-Tag noindex always;`;博客 vhost 加 `location /admin/ { add_header X-Robots-Tag noindex always; }`;`public/robots.txt` 新建;ESA 全站 purge + HTML TTL 评估 ≤1h。
- 验收:`curl -sI https://wait4x.online/ | grep -ci x-robots-tag` = 0;`curl -s https://wait4x.online/robots.txt` 含 `Sitemap: https://wait4x.online/sitemap-index.xml`;`curl -sI https://wait4x.online/admin/ | grep -ci noindex` = 1;Lighthouse 生产 home SEO ≥95。
**B|localePath 双 en + canonical + 尾斜杠**(0.5d;风险低,纯前端)
- 做法:`src/lib/locale.ts` localePath 先归一到 zh 路径再拼;`BaseLayout.astro` 加 `<link rel="canonical" href={new URL(withSlash(rawPath), Astro.site)}>`;Header nav-lang href 与所有站内链接统一带尾斜杠。
- 验收:`grep -c '/en/en/' dist/en/archives/mian-sha/index.html` = 0;`dist/` 内每页有自指 canonical;playwright `link[hreflang="en"]` 的 href 与自身 URL 相等;`curl -sI https://wait4x.online/en/archives/mian-sha`(无斜杠)→ 301 或 canonical 唯一。
**C|构建期内容 lint + 生产冒烟**(1d;风险:lint 误报阻断发布→先 warn 一周再 fail)
- 做法:`scripts/build-perf/content-lint.mjs`(断链:a href 站内前缀逐一对 dist 产物;图片存在性;frontmatter typebox schema;zh/en 配对警告);`build.sh` 守卫挂钩 `[ -f scripts/build-perf/content-lint.mjs ] && node … || echo skip`(铁律:存在性守卫必须是第一笔改动);生产冒烟脚本 curl robots/sitemap/首页头三断言。
- 验收:人为引入断链 md → build 输出 FAIL 清单;正常仓 build 通过;`sh infra/deploy/build.sh` 全链绿。
**D|/draft/ 预览路由**(1d;风险:草稿 URL 泄露→长随机段+noindex+不入 sitemap)
- 做法:posts 集合 frontmatter 加 `draft: boolean`;`src/pages/draft/[slug].astro`(draft:true 文章构建进 `/draft/<token>/<slug>/`,token=构建期环境变量注入的随机串);getStaticPaths 正式路由已过滤 draft(`!data.draft` 现成);robots.txt Disallow /draft/;nginx 无需改(静态目录直出)。
- 验收:draft 文章不出现在 `/archives/` 与 rss.xml/sitemap;`/draft/<token>/<slug>/` 200;`curl -sI …/draft/ | grep -ci noindex` = 1;playwright 断言 draft 页无 nav 中"文章"aria-current。
**E|reader 纯文本模式**(0.5d;风险:覆盖不全→按 §4.4 清单逐条断言)
- 做法:`html.reader` 单 class(global.css 尾部集中一段,全部 `html.reader xxx` 前缀);CommandPalette 加 `data-cmd="reader"` 词条(i18n/ui 双语);Keyboard `r` 单键;localStorage `reader` 持久化。
- 验收:playwright 触发命令后 `.bg-glow`/`.read-progress` 不可见、备案号可见、正文段落数不变;`html` 有 `reader` class 且刷新(reload)后仍在;E2E zh/en 各跑一遍。
**F|列表评论数 + 阅读时长角标**(1d;风险:Artalk 不可用时构建卡死→超时 3s 降级 `–`,lint warn 不 fail)
- 做法:`scripts/build-perf/artalk-counts.mjs` 构建期对每篇 `GET 127.0.0.1:13030/api/v2/comments?…&site_name=wait4x.online&limit=0` 取 count 写 `dist` 内联进 props(或 src/data/ 快照 JSON,构建前生成);PostList.astro li 加 `<span class="cnt">N</span>` 与阅读时长角标(复用 `[...slug].astro:39` 的汉字/400 算法抽成 lib);首页"最近心跳"旁"最新评论"流(§4.3 两路择一)。
- 验收:构建日志含每篇 count 拉取;dist 列表 HTML 含 `class="cnt"`;断 Artalk(停容器 30s)build 仍成功且角标 `–`;playwright 列表页可见"N 条"与"~8 min"。
**G|gallery EXIF 元数据卡**(1d;风险:老图无 EXIF→字段缺省隐藏不渲染空行)
- 做法:photos_ingest.py 产物已含 exifr 数据,确认 JSON 字段(机型/焦段/光圈/ISO/时间/地点)→ gallery 详情/灯箱下加等宽字体表(mono,ink-3);nbfox 式"元数据即说明"。
- 验收:任一有 EXIF 的图渲染 6 字段中 ≥4;无 EXIF 图无空表壳;E2E 断言 mono 类名。
**H|home title/description 语义化 + RSS 可见性**(0.5d)
- 做法:home `<title>wait4x · 阅读 技术 摄影</title>` 级别语义;`/archives/` 顶部加一行 RSS 提示(xeiaso 式,链 /rss.xml 与 /en/rss.xml);description 按页级注入(BaseLayout 已有 prop,逐页补实)。
- 验收:playwright 断言每类页 title 唯一;archives 页含 rss 链接且 hreflang 正确。
**I|瞬间编号化 + 友链页**(各 0.5d)
- I1 moments 每条构建期序号 `#N`(diygod/qh1009/阮一峰三方验证的节奏感)+ 页顶"本周瞬间"聚合块(可选)。
- I2 `src/pages/links.astro` + `content/links.yaml`(name/url/desc/avatar),导航不加项(克制),入口放 about 与页脚。验收:links 页渲染全部条目且 a[href] 全 200(lint 断链覆盖)。
**J|偏好雏形 + craft 编年页(观望)**:reader/辉光/动效三 toggle 收进命令面板 `settings` 分组(不做独立面板 UI);terminal 页扩"工艺案例编年体"(rauno /craft 式)列为远期。邮件复制微交互(rauno "Copied")0.2d 可顺手做进 Footer。
## 6. 排期建议(三批)
- **立即(本周,SEO 解封是硬闸门)**:A → B → C(C 的生产冒烟依赖 A/B 上线)→ E;ESA purge 随 A 同日执行。依赖:A 需 homelab nginx 操作权限(用户或运维车道);ESA 控制台 purge 需用户操作单。
- **本月**:D(draft 路由)→ F(评论数,依赖 Artalk 在线策略确认)→ G(EXIF)→ H;尾单:删 media-sample。
- **观望(触发条件)**:I1/I2(内容节奏成型后)、J(terminal 页改版时合并)、Artalk 2.10.1 升级(下个维护窗口)、Newsletter(RSS 订阅量证明需求后)、GitHub PAT(热力图真实数据,用户素材依赖,scratchpad 在案)。
- 用户操作单(非代码):① ESA 全站 purge + HTML TTL 调整 + /artalk/api 绕缓存规则复核;② GitHub PAT(热力图);③ nginx 重载窗口确认。
## 7. 明确不做的(与灰阶克制冲突,v1 P2 续封 + 新增)
- 背景音乐/一言/背景特效(v1 P2,innei 同款):稀释终端隐喻一致性。
- 首页大图专题卡/瀑布流重化(v1 P2,nbfox 同款):内容电商型视觉,与"灰阶骨架×分轨正文"定稿相悖。
- 常驻浮动 TOC/阅读进度大改(v1 P2):现 .toc 桌面 sticky 已够,长文少。
- **新增:评论数实时化/SWR**(构建期快照已够,实时=引入运行时查询+ESA 缓存语义复杂度)。
- **新增:AI persona 注入彩蛋进正式页**(diygod 同款虽有趣,但正式内容层注 prompt 有被搜索引擎误读风险——只进 terminal 彩蛋页)。
- **新增:独立设置面板 UI**(innei 式面板):命令面板分组已覆盖收纳需求,独立面板是 UI 负担。
## 附:本轮实测命令与产物索引
- 生产:`curl -sI https://wait4x.online/{,archives/mian-sha/,robots.txt,admin/}`(noindex 头、404、30 天缓存头);`/sitemap-0.xml` 34 URL 全 200。
- 本机:`/etc/nginx/snippets/esa-origin-ssl.conf`、`sites-enabled/default:92`、`docker_apps/artalk/data/artalk.yml`、`127.0.0.1:13030/api/v2/comments?…` 响应原文。
- 外站快照:/tmp/v2/*.html(会话级):rauno.me(含 /craft /projects)、diygod.cc(含 /posts)、xeiaso.net(含 /blog)、shud.in、vincentwill.com、sabrinacodes.com、artalk.org(易主证据);brianlovin.com=Vercel 挑战页(失败标注)。
- 仓库:BaseLayout.astro / locale.ts / PostList.astro / CommandPalette.astro / ArtalkComments.astro / [slug] 与 [...slug].astro / archives/index.astro / build.sh / global.css / tokens.css。
+204
View File
@@ -0,0 +1,204 @@
# 博客美化专项方案(视觉打磨批次)
> 侦察员 @agent-591c3f | 2026-10-26
> 证据链:仓库源码逐文件审计(tokens/global.css + 9 组件 + 2 文章页)× design/mockup.html 丙路线对照 × 本地重建 34 页实测(dist 内 Shiki 内联样式实抓)× WCAG 对比度数学计算 × 5 站补充抓取。
> 截图证据床 /tmp/beauty-shots/(22 张,1280×900 fullPage,SW 阻断,gz-server 18800 测试床):现状 12 张 `{home,reading,tech,books,gallery,moments}-{light,dark}.png`;mockup 基线 10 张 `mock-{home,reading,tech,gallery,moments}-{light,dark}.png`。
> 纪律分工:token 值/选择器/对比度数字=本报告结构性结论;"观感好坏"=⚠视觉待验,交视觉模型按 §5 问题清单复核,卡片产出不受阻。
## 0. 补充探索(每站聚焦视觉细节)
**抓取失败如实标注**:veselinov.pro、xivudong.me、photos.antfu.me 三域本机 DNS 无解析(getent 空记录,代理 7897 亦 000),已按任务预案换站并注明理由。
1. **rauno.me** ✅(HTML+2 CSS 实抓)——灰阶密度才是质感:stitches 体系 12 级灰(gray1–12 双主题 hsl 全套);间距阶严格 8px 步进(--space-1..11 = 8→88px);圆角三档 4/8/16+round;阴影三档全部 12% 黑透明(阴影当层级语言而非装饰);全站唯一动效曲线 `cubic-bezier(.2,.8,.2,1)`;focus-ring 变量化。字体=自研 X + JetBrains Mono + Georgia 点缀。
2. **paco.me** ✅(3 CSS 实抓)——灰阶走**黑/白 alpha 阶梯**(--black-a1..a12 = 5%–95% 共 12 档):灰由前景色透明度派生,暗色主题零重复维护;Newsreader 斜体做强调词是签名手法;字号全站仅 13/14/16/1.25rem/4rem 五档;边框唯一 `--border-thin:1px`;页边距 `max(24px, env(safe-area-inset-*))` 安全区与留白合一。
3. **mattdesl.com** ✅(换站理由见上)——全站等宽身份(BerkleyMono→Courier 回退)+ 97% 灰底/10% 墨字;标题 font-weight **normal**、统一 margin-top 1.5em——层级全靠字号与节奏不靠字重;标题小写化;标题锚 `#` 前缀常显;a:active 才变灰。与我们的 mono 轨同源:等宽当身份时,字重必须让位给间距。
4. **tw93.fun** ✅(替代 xivudong.me,DNS 失败)——body 15px,身份字体藏锋金楷 TsangerJinKai02(标题正文混排),中文个人站"手写体温"代表;内联 prefers-reduced-motion 全量豁免(与我们同款纪律);header 图标化导航。启示:中文身份字体走金楷/文楷一脉成立,我们 `--font-kai` 已在栈内,金句正是它的岗位。
5. **antfu.me/photos** ✅(photos 子域 DNS 失败,主域 /photos 200)——照片 img 带 inline `radial-gradient` **blur-up 占位**:图未加载先出主色近似色块,加载即有构图而非白洞;导航相机图标常驻;照片**无圆角无边框无阴影**直贴(摄影=内容本体不加框)。启示:图库质感优先做加载占位,而非边框阴影装饰。
## 1. 现状视觉审计(元素/现状/差距/目标)
| # | 元素 | 现状(源码事实) | 差距 | 目标 |
|---|---|---|---|---|
| 1 | 间距阶 | `--space-1..6`=4/8/16/24/40/64,跳档不均(缺 12/32) | rauno 8px 步进全阶 | 主阶不动,补正文流距 token(K) |
| 2 | 标题节奏 | `.prose-* h2/h3` 仅 margin-top 1.8/1.5em,**无下距**(global.css:69-79) | 标题贴死正文;mockup h2 为 `1.8em 0 .6em` | h2/h3 补下距(K) |
| 3 | 正文段距 | `p+p{margin-top:.9em}` 双轨 6 处字面量 | 不可一处调 | `--flow-*` token 化(K) |
| 4 | 灰阶层级 | ink-1..4+line 五档;**ink-4(2.52:1) 用于 13px 日期/图注正文级文字**(PostList row-date、tl-date、g-note)不达 AA;无 border-strong 档 | ink-3 5.09:1 达标;ink-4 应退纯装饰位 | ink-4 停用于文字,新增 `--line-strong`(L) |
| 5 | 圆角 | 4/6/8/10/12/999 字面量散落 7+ 处组件 | 未 token | `--radius-s/m/l` 三档(O) |
| 6 | 动效时长 | 0.15s/160ms/0.4s/0.6s/2.4s 与 `--dur:350ms` 并存;**且 `.postlist .row` hover 1px 上浮是死规则**(global.css:196 类名应为 `post-list`,dist 实证从未生效) | 时长纪律散 + 微交互断线 | 修死规则 + `--dur-fast/slow` 补阶(O) |
| 7 | 书评纸面板 | `.prose-reading` 仅上下 hairline + bg(global.css:52-63) | mockup 丙=全边框圆角卡(pad 44/radius 14)形态差 | 两案裁定(Q)⚠视觉待验 |
| 8 | 代码块 | Astro 默认 Shiki github-dark,dist 实测内联 `background:#24292e` **压过 token `--code-bg:#0d1117`**;无语言标签;`.term-dots` 为死选择器 | token 与实际分裂 | Shiki css-variables 主题接管 + 灰阶窗条 + 语言标签(P) |
| 9 | 表格 | global.css **零表格规则**;内容 3 篇含 markdown 表(dist 实证 `<table>` 裸渲染) | UA 默认裸表 | 分隔线制表格样式(R) |
| 10 | 列表 | reset `*{margin:0}` 后无 prose 列表规则;内容 5 篇含列表 | li 间距仅靠行高,紧贴 | li+li 间距 + marker 色(R) |
| 11 | 金句 | `.pull-quote` 全仓无使用(死选择器);blockquote 只有 A 墨线模式 | D 金句模式从未落地 | 写作指南模板 + mood 注入(Q) |
| 12 | 引文规则 | global.css 与 [...slug].astro:172-177 **镜像双份** | 漂移风险 | 单源化(Q) |
| 13 | 图库衬底 | `.stage` 仅 container-type,无背景 | mockup `--stage:#f4f4f2` 面板+radius 14 | 两案裁定(N)⚠视觉待验 |
| 14 | 页面标题 | books/gallery/moments 用 `--text-h1` 27px | mockup g-title 20px/700 | 降一档 22px 或保持(K)⚠ |
| 15 | 首屏 | IdCard=60px 渐变头像(紫→accent 彩色占位)+21px 名字,slot 被 socials 占用,**无身份陈述行** | mockup hero 有 bio 15px 行 | M 两案 |
| 16 | 热力图 | ink 灰四档(组件注释明示决策) | mockup 原型为绿四档 | S 裁定 ⚠视觉待验 |
| 17 | 达标项(不动) | 灯箱 backdrop 95%、focus ring 全局 2px、::selection 半透明、明暗双套 tokens、reduced-motion 全豁免(row hover 上浮除外——行 6 实锤为死规则) | — | 保持 |
## 2. 美化卡片(K–S,续 v2 A–J)
### K|正文流距 token 化与标题下距
- **目标**:标题-正文、段落-段落的呼吸成为可调参数;治愈 h2 贴正文。
- **改法**(tokens.css + global.css):新增 `--flow-p: 0.9em; --flow-h2-pre: 1.8em; --flow-h2-post: 0.6em; --flow-h3-pre: 1.5em; --flow-h3-post: 0.45em;`;global.css 双轨 h2/h3 改用变量并补 margin-bottom;`p+p` 改 `var(--flow-p)`。页面标题(Books/Gallery/Moments h1.page-title)27px→`--text-h2`(22px) 降档对齐 mockup g-title(视觉待验§5-6)。
- **涉及**:`src/styles/tokens.css`、`src/styles/global.css`、三页组件 `page-title` 规则。
- **验收**:playwright 断言 `.prose-tech h2` computed marginBottom ≈13.2px(22×0.6)、marginTop≈39.6px;`:root` `--flow-p`===' 0.9em';books 页 `h1.page-title` fontSize==='22px'。
- **工作量**:0.5d。
### L|灰阶达标与 border-strong
- **目标**:全部正文级文字 ≥4.5:1(WCAG AA 实测数:ink-3 亮 5.09/暗 5.22 ✓,ink-4 亮 2.52/暗 2.67 ✗);分隔线获得强弱两档。
- **改法**:PostList `.row-date`、MomentsPage `.tl-date`、GalleryPage `.g-note`、index `.lab` 的 `color: var(--ink-4)`→`var(--ink-3)`;tokens 新增 `--line-strong: #d4d6db` / dark `#3d434f`,供 kbd 边框、chip 选中态(现用 ink-3 充当)、TOC 选中线。ink-4 仅保留纯装饰(track 前方块、beat-dot)。
- **涉及**:tokens.css、PostList.astro、MomentsPage.astro、GalleryPage.astro、index.astro。
- **验收**:`.row-date` computed color==='rgb(107, 110, 120)';对比度数学 5.09:1;`--line-strong` 存在且 chip:checked border-color===rgb(212,214,219)。
- **工作量**:0.5d。
### M|首屏叙事两案(⚠视觉待验 §5-1 定案)
- **案1 极简维持+身份陈述**(推荐先行):IdCard 结构改两行——名字 21px 下加 `.idcard-bio`(15px/ink-3,一句身份陈述,文案用户定);socials 移到 bio 行下;头像渐变占位灰阶化(`--mood-3/accent` 渐变→`ink-3→ink-1` 灰渐变,真头像接入前不破纪律)。`margin-block` 40→56px 加呼吸。
- **案2 hero 强化**:头像 72px、名字 `--text-h1` 27px、bio 一行 mono 14px tagline(如 `阅读 · 技术 · 摄影`),首屏区块底加 `--line` 分隔。视觉重量↑,与"骨架克制"张力↑。
- **涉及**:IdCard.astro、Avatar.astro、index.astro。
- **验收**:案1 `.idcard-bio` 存在且 fontSize==='15px'、color===ink-3;头像 svg 首个 stop 颜色为灰阶(无 hue)。
- **工作量**:案1 0.5d / 案2 1d。
### N|图片质感与灯箱纯度
- **目标**:照片墙获得"策展台"层次而非散图直贴;灯箱进入"暗房"纯度。
- **改法**:tokens 新增 `--stage: #f4f4f2` / dark `#101318`(mockup 丙同位);GalleryPage `.stage` 加 `background: var(--stage); padding: var(--space-3); border-radius: var(--radius-l, 14px)`(⚠两案:有衬底 vs 无衬底,§5-4);新增 `--radius-img: 10px` 统一 .shot/.thumb/.mom-fig/prose img 圆角(现 8/8/8/10 混用);灯箱 backdrop `rgba(8,10,14,.95)`→`.97` + 底条 mono 字距 `0.02em`;blur-up 占位列观望(antfu 手法需管线配合,非纯 CSS)。
- **涉及**:tokens.css、GalleryPage.astro、MomentsPage.astro、index.astro、global.css。
- **验收**:`.stage` computed backgroundColor==='rgb(244, 244, 242)';`:root` `--radius-img` 存在且 `.thumb` borderRadius==='10px';dialog#lb backdrop 不便断言→断言 CSS 源含 0.97。
- **工作量**:1d。
### O|微交互 token 化与 focus ring 收尾
- **改法**:先修死规则——global.css `.postlist .row`→`.post-list .row`(hover 1px 上浮即刻复活);tokens 新增 `--dur-fast: 150ms; --dur-slow: 600ms;`;替换字面量——row hover `0.15s ease`→`var(--dur-fast) var(--ease)`、GalleryPage `#lb-img` 160ms→`var(--dur-fast)`、`.bg-glow` 0.6s→`var(--dur-slow)`、rise-in 0.4s→`calc(var(--dur) + 50ms)`;圆角 token:`--radius-s:6px; --radius-m:10px; --radius-l:14px`,全局字面量归位;`:focus-visible` 补 `border-radius: 2px`(mockup:29,防直角 outline 扎圆角钮)。
- **涉及**:tokens.css、global.css、Header.astro、GalleryPage.astro、各组件 radius 字面量。
- **验收**:`.post-list .row` computed transitionDuration==='0.15s' 且 hover 后 transform==='matrix(1, 0, 0, 1, 0, -1)' 且 timingFunction 含 'cubic-bezier(0.19, 1, 0.22, 1)';`:focus-visible` computed borderRadius==='2px';`grep -c 'px' radius 字面量` 下降(构建审计)。
- **工作量**:0.5d。
### P|代码块窗口化(Shiki 接管 + 语言标签 + 灰阶窗条)
- **目标**:`--code-bg/--code-ink` token 恢复统治(现被 Shiki 内联 #24292e 压制);代码块获得窗口身份。
- **改法**:astro.config.mjs `markdown.shikiConfig.theme: 'css-variables'`(Shiki 官方主题,颜色全走 `--shiki-*` 变量);global.css 定义 `--shiki-default/--shiki-key/...` 对齐现有 github-dark 色系但 bg/ink 用 `var(--code-bg)/var(--code-ink)`,亮暗同底纪律恢复;`pre.astro-code[data-language]::after{content:attr(data-language); …mono 11px; color:#8b949e; text-align:right; display:block}` 语言标签(零 JS,dist 实证 data-language 属性已在);窗条三灰点用 `pre::before{content:'';display:block;width:10px;height:10px;border-radius:50%;background:#3f4650;box-shadow:16px 0 0 #3f4650,32px 0 0 #3f4650;margin-bottom:8px}`(灰阶三档 `#3f4650/#4a515b/#565f6a` 递进,macOS 彩点不采——红线);pre padding 顶加 `6px`。删除 `.term-dots` 死规则。**行号不做**(短命令块为主,320px 移动端宽度宝贵,见 §4)。
- **涉及**:astro.config.mjs、global.css。
- **验收**:dist 文章页 `pre.astro-code` 无内联 background-color(`getAttribute('style')` 不含 'background-color');pre computed backgroundColor==='rgb(13, 17, 23)';`getComputedStyle(pre,'::after').content` 非 'none' 且含 attr;`getComputedStyle(pre,'::before')` width==='10px'。
- **工作量**:1d。
### Q|书评轨:纸面板形态 + 金句实装 + 引文单源
- **目标**:书评页的"纸感"从色块升级为形态;D 金句模式真正可用。
- **改法**:
- 纸面板两案:**A 卡化**=`.prose-reading{border:1px solid var(--paper-line);border-radius:var(--radius-l);padding:var(--space-5) clamp(20px,4vw,32px)}`(mockup sheet 形态);**B 维持通栏** hairline 仅把上下线升级 `--line-strong`。(⚠§5-2 裁定)
- 金句:内容约定 HTML 直书 `<blockquote class="pull-quote"><p>…</p><span class="quote-cite">——作者《书名》</span></blockquote>`(写作指南已有 raw HTML 先例);pull-quote 样式修 62px→`3.2em` 相对化 + `--mood` 从文章 frontmatter 经 [...slug].astro `article style="--mood:…"` 注入(现 PostList 已用同源 mood,接线一致);docs/写作指南.md 加模板一节。
- 引文单源:删除 [...slug].astro:172-177 镜像规则,markdown 裸 blockquote 全靠 global.css。
- **涉及**:global.css、pages/archives/[...slug].astro、en 同文件、docs/写作指南.md。
- **验收**:`grep -c 'blockquote' src/pages/archives/[...slug].astro`===0;含 pull-quote 的页面 computed fontFamily 含 'WenKai'、`::before` fontSize==='64px'(20×3.2);A 案 `.prose-reading` computed borderRadius==='14px'。
- **工作量**:0.5d。
### R|表格与列表基建(从零到精)
- **改法**(global.css,作用于 `.prose-reading/.prose-tech`):表格 `table{border-collapse:collapse;width:100%;font-size:.92em;margin-block:1.2em} th{text-align:left;font-weight:600;color:var(--ink-1)?→书评轨用 paper-ink} th,td{padding:6px 12px 6px 0;border-bottom:1px solid var(--line)} tbody tr:last-child>*{border-bottom:0} td{font-variant-numeric:tabular-nums}`——分隔线制,**不做 Zebra**(灰阶密度已够,见 §4);列表 `ul,ol{padding-left:1.5em;margin-block:1.2em} li+li{margin-top:.35em} li::marker{color:var(--ink-3)}`;tech 轨 kbd 已有样式保持。
- **涉及**:global.css(含 3 篇含表内容无需改动)。
- **验收**:tech 文章页 `th` computed borderBottomWidth==='1px';`li+li` marginTop≈6.65px(.35em@19px);`li::marker` color==='rgb(107, 110, 120)'。
- **工作量**:0.5d。
### S|热力图色系裁定(⚠视觉待验 §5-5)
- **现状**:ink 灰四档(hm-l1..4=ink-4..1),组件注释明示"灰阶纪律"决策;mockup 原型为 GitHub 绿四档。绿=贡献量语义直觉(外部认知零成本),灰=骨架纯度。
- **改法(若裁定换绿)**:tokens 新增 `--hm-0:#ebedf0/--hm-1:#9be9a8/--hm-2:#40c463/--hm-3:#30a14e/--hm-4:#216e39` + dark 套(#22272e/#0e4429/#006d32/#26a641/#39d353),Heatmap.astro 四行替换。默认建议**维持灰阶**(热力图是骨架层不是内容层;绿色块会成为首页唯一大面积彩色,重心被劫持)。
- **验收**:若换绿 `--hm-4`===' #216e39' 且 `.hm-l4` backgroundColor 对应;维持则零改动,S 卡关闭。
- **工作量**:0.2d。
## 3. 优先级(视觉收益 × 工作量)
| 卡 | 视觉收益 | 工作量 | 象限 |
|---|---|---|---|
| L 灰阶达标 | 高(可读性硬指标) | 0.5d | ★高收益低成本 |
| R 表格列表 | 高(3+5 篇内容直接受益,现裸样式) | 0.5d | ★ |
| K 流距/标题下距 | 高(全文页节奏) | 0.5d | ★ |
| M 案1 首屏陈述 | 高(第一屏人格) | 0.5d | ★ |
| P 代码块窗口化 | 高(技术轨视觉主件) | 1d | ★(成本略高但收益覆盖全部技术文) |
| Q 纸面板+金句 | 中高 | 0.5d | 次批 |
| O 微交互 token | 中(一致性负债清偿) | 0.5d | 次批 |
| N 图库质感 | 中 | 1d | 次批(含⚠两案) |
| S 热力图 | 低(大概率维持现状) | 0.2d | 随批 |
**前 5(高收益低成本)**:L → R → K → M案1 → P。合计 ≈3d,其中 L/R/K 纯 global+tokens 层,一次提交可收。
## 4. 明确不做(与灰阶克制红线冲突,含理由)
1. **玻璃拟态/大面积毛玻璃**:backdrop-filter 大面积使用与"纸墨实体感"冲突;微信内置浏览器兼容与滚动性能翻车史;Header 8px blur 轻量例外保留。
2. **渐变色文字/渐变大块**:全站唯一强调色(--accent 单色)是丙路线根基;渐变=多色并存,01 报告诊断的多色病复发。
3. **大圆角卡片风(≥16px)**:圆阶上限钉 `--radius-l:14px`;大圆角是消费 App 友好风,与终端/制版感相反。
4. **macOS 三彩圆点/彩色窗条**:代码窗条采灰阶三档(P 卡);彩点是全站唯一彩点的地位争夺者。
5. **噪点/纸纹覆盖层**(02 报告 Tier1 曾列,本批否决):纸感已由 `--paper` 米白承担;噪点在低分屏/微信内染摩尔纹,reduced-motion 需额外豁免,收益/成本比不成立。
6. **表格 Zebra 斑马纹**:隔行灰块密度过高,与分隔线骨感二选一取线(R 卡)。
7. **代码行号**:内容以短命令块为主,行号吃掉 320px 移动端宽度;Shiki attrs 可后补不锁死。
8. **行尾点号悬挂/首行缩进**:03 报告已证伪(无出版惯例/避头尾风险)。
9. **弹跳缓动/大面积视差/入场加码**:红线"不弹跳宁轻勿闹";现有 12px rise-in + 1px hover 是上限。
10. **自定义光标**:02 Tier3 已判,正文必须标准 I-beam。
## 5. 截图问题清单(⚠视觉待验——交视觉模型,附证据文件)
| # | 问题 | 证据对 |
|---|---|---|
| 1 | 首屏 M 案裁定:现状 IdCard+列表直连的密度 vs mockup hero 的呼吸,哪个更"被设计过"?案2 hero 是否过度? | home-{light,dark}.png ↔ mock-home-{light,dark}.png |
| 2 | 纸面板 Q-A/B:通栏 hairline(现状)vs 全边框圆角卡(mockup),哪个更"书页"?暗色下 paper #f8f6f1↔#201d18 转换是否自然? | reading-{light,dark}.png ↔ mock-reading-{light,dark}.png |
| 3 | 代码块 P:加灰阶窗条+语言标签后是否加分?(可先看 mockup term 窗对比现裸块) | tech-{light,dark}.png ↔ mock-tech-{light,dark}.png |
| 4 | 图库 N:stage 米灰衬底 vs 直贴,哪个衬托照片更好? | gallery-{light,dark}.png ↔ mock-gallery-{light,dark}.png |
| 5 | 热力图 S:灰四档 vs 绿四档的量级辨识与首页色彩重心 | home-{light,dark}.png(热力图区) |
| 6 | 页面标题降档(27→22px):books/gallery/moments 三页标题现值是否过重? | books-{light,dark}.png(无 mock 对照,独立判断) |
| 7 | 现状整体灰阶层级是否"够用"(rauno 12 档 vs 我们 5 档):列表行 hairline+日期档位是否单薄? | home-*.png 列表区 ↔ mock-home-*.png |
## 附:测试床与复现
- 截图环境:`npm run build`(34 页)→ `python3 scripts/build-perf/gz-server.py 18800 dist`(写稿时仍在跑,用后可杀:`pgrep -f gz-server` 取 PID kill);脚本 `/tmp/beauty-shots/shoot.cjs` 与 `shoot-mockup.cjs`(`NODE_PATH=$(pwd)/node_modules node …`)。
- 对比度计算:sRGB 相对亮度公式手算(§1-4 数值即此口径),可直接进验收注释。
## 6. 视觉裁定结果(glm-5.3-flash:max 视觉轨,2026-10-26 定案)
> §5 七问全部裁定完毕,⚠视觉待验标记以此为准失效;卡片 M/N/S 的两案之争已定。
裁定基准:灰阶克制(不彩色不弹跳宁轻勿闹)· 专业质感(被设计过≠被装饰过)· 双模式同判。
证据:/tmp/beauty-shots/ 现状 12 张 + mock 10 张全部读图;另对书评页头部(亮/暗)与热力图区(亮/暗)做 2× 特写裁片 4 张复核细节。
---
## #1 首屏:现状 IdCard 直连 vs mock hero
**裁定:两案融合**(取 mock 的 tagline + 标题升一档;保留现状的信息密度与紧凑行距,不 banner 化)| 信心:高
理由:现状 IdCard(头像+名+3 图标)到 2026 列表之间无一句身份语,读感偏"档案页";mock hero 仅一行「写作者 · 读者 · 快门按压者,拥抱开源。」+社交图标,无插画无 CTA,克制成立——不过度、不空洞,且"被设计过"确实更强。
落地注意:tagline 控制在 ≤20 字级,移动端随 hero 收紧行高;禁加背景辉光、大字标语等 banner 元素。
## #2 书评纸面板:通栏 hairline vs 圆角纸卡
**裁定:选 mockup**(保留现状"标题区外置"结构;正文列由 hairline+微纸底升级为圆角米灰纸卡)| 信心:高
理由:现状正文列只有极淡暖底 + 顶部通栏 hairline,"一页纸"的物体感弱;mock 的细边框+圆角+米灰底让纸卡成立。暗色不冲突:现状暗列与 mock 暗卡都已转暖棕纸色(夜灯感),转换自然。
落地注意:本文实测 7000px 长文——卡片左右边框在视口内就是两根竖线,边框用 1px hairline 级、圆角收敛;移动端收 padding 保字宽。
## #3 代码块:裸块 vs 终端窗条
**裁定:选 mockup(窗条加分),但只给多行块** | 信心:高
理由:现状暗色技术页一排单行裸块退化为"纹理条",块与底仅靠细边框区分;mock 窗条提供 `bash · deploy@vps` 标题(是信息不是装饰)+ 一条灰阶色带,暗色下块轮廓明显更立。
落地注意:≥4 行的块才挂窗条,单行命令保持裸 pill;红黄绿三点可换灰阶三点——窗条+mono 标题已足够传达终端梗,守住红线更稳。
## #4 图库衬底:直贴 vs stage 米灰衬底
**裁定:两案融合**(亮色维持白底直贴;暗色给照片加轻衬底/描边)| 信心:中
理由:亮色现状就是干净的白盒画廊(21 张彩照自供色),灰衬板会成全页最大 UI 块、抢照片的戏;暗色实拍夜景照(明珠塔/暗楼群/暗日落)边缘融底,轻衬底恰好能救。mock 图库两图均截在动画中途(扇形堆叠+页脚透字),终态布局证据不足。
落地注意:暗色衬底用 ~4% 白底或 1px hairline,衬底圆角不得大于照片圆角,避免"框中框"。
## #5 热力图:灰档量级辨识 & 绿系利弊(只评不站队)
**裁定:双模式分别判——亮色灰档够用;暗色低两档实缺。绿系利弊各半** | 信心:高
理由:2× 特写下亮色灰档可辨、密集黑簇(周峰值列)一目了然;暗色最低两档与底色粘连、接近丢失。绿系=明度+色相双编码、暗色读数大增、geek 熟悉感;代价是成为全页最大色块,与照片条/彩点连成"颜色带",压灰阶骨架重心、违"宁轻勿闹"。
落地注意:若动,只动暗色或用去饱和苔绿(sage);亮色不建议换绿。
## #6 书架页标题 27px 是否过重
**裁定:维持现状(不过重,不降到 22px)** | 信心:中
理由:书架页唯一层级锚点,27px 与 17px 级书名卡的差距正是层级本身;降到 22 与卡题贴脸,稀疏页面会更散。暗色下白字光晕(halation)放大视觉重量,是"显得重"的主因。
落地注意:与图库/文章页标题保持同轨字号(一致性优先于单页微调);要轻就减字重(700→600)不减号。
## #7 灰阶层级:hairline+日期单薄?5 档够不够
**裁定:两案融合**(hairline 保留但更淡,行距向 mock 靠拢;5 档灰够用,不追 12 档)| 信心:高
理由:现状 hairline+右对齐日期是"档案感"资产,并不单薄;mock 全靠行距分组更透气,但长列表会失锚。页面可感知灰阶本就 4-6 阶,rauno 的 12 档是设计体系储备,页面级硬凑只会出"渐变灰"噪感。
落地注意:暗色 hairline 再降一档亮度(现状略亮、切分感偏强);行距 +2~4px 优先于删线。
---
## 三行总评
1. **当前视觉成熟度:约 7.5/10**——已是完整自洽的灰阶克制系统(暖纸正文列/彩色书脊卡/mono 元数据/双模式纸色转换),缺口集中在 hero 身份语、暗色低灰阶辨识度、代码块"仪器感"。
2. **最值的一张卡:代码块终端窗条**——信息(上下文标题)+质感双收益、暗色收益最大、实现风险最低;书评纸卡次之。
3. **最危险的过度设计倾向:把画廊"道具化"**(stage 衬底+扇形堆叠动效是 mock 截图里已现形的方向)——与 hero banner 化、绿热力图同属"为设计感加戏",画廊应是白盒,动效层越薄越贵。
## 证据瑕疵备注
mock-home-dark 列表缺《海风中失落的血色馈赠》一行(截图态);mock 图库亮/暗两图均抓在动画中途。以上不影响各问主证据链,仅 #4 信心降为中。
+119
View File
@@ -0,0 +1,119 @@
# 灯箱美化探索:图片查看体验天花板五站研究(v2.1 输入)
> 只读调研 + 提案,不动代码。基线 = 灯箱 v2 车道(fit-contain / 滚轮锚点缩放 0.5–5x / 拖拽平移 /
> 双击 1x↔2.5x / caption+EXIF 分层 / ←→ 与触摸快滑),提案全部**叠加在 v2 之上,不推翻缩放引擎**。
> 配套可交互演示:`design/lightbox-mockup.html`(浏览器直接打开,交互为提案演示)。
> 标记:✅ = 静态抓取并核对源码/文档;◐ = 站点可达但查看器 JS 渲染,基于已知稳定形态记录。
## 一、五站笔记
### 1. PhotoSwipe(photoswipe.com)✅ 业界标杆 lightbox
- **A 开合过渡**:`showHideAnimationType: zoom`(默认)= 从缩略图原位起飞到舞台、关闭回落原位;
无缩略图时降级 fade;`prefers-reduced-motion` 自动 none。时长 333ms ×3(show/hide/zoom 同拍),
曲线 `cubic-bezier(.4,0,.22,1)`,双击缩放共用同曲线(节奏统一的关键)。
**cover 裁切缩略图路线**:`object-fit:cover` + `data-cropped` —— 我们的 masonry 正是这种缩略图。
- **B 舞台纯净度**:底色实色 + `bgOpacity`(默认 .8,透明度走 JS 合成不走 rgba,性能纪律);
文档明示**禁止 backdrop blur**(重性能坑)。`tapAction:'toggle-controls'`:单击即隐全部 chrome,纯看图。
- **C 信息层**:核心无 caption(a11y 原因删出 v5),官方 dynamic-caption 插件——"依剩余空放置于
图下或图侧":竖图自动把 caption 挪进侧边 letterbox,横图回底部。计数器 `n / N` 极简。
- **D 导航**:边缘箭头 + loop + `arrowKeys`;幻灯片间距 `spacing:.1`(10% 视口,换片有"路过"感);
`allowPanToNext`:放大态继续滑 = 翻页(先耗尽平移余量再走片)。
- **E 微交互**:`preload:[1,2]` 相邻预载;`preloaderDelay:2000`——加载指示 2s 后才出现(快图不打扰);
`maxWidthToAnimate:4000` 超宽图不做起飞动画(防掉帧)。图标自带描边阴影,深浅底通吃。
- **F 移动端**:`pinchToClose`(捏合关闭)、`closeOnVerticalDrag`(竖拖关闭)、双指缩放到 max 档;
`trapFocus + returnFocus`(焦点圈与归还,原生 `<dialog>` 免费同款)。
### 2. medium-zoom(francoischalifour.com)✅ Medium 的克制哲学
- **A**:点击 = 从缩略图位置 FLIP 放大到视口居中(margin 默认 0 贴边),关闭回落原位。
与 PhotoSwipe 同为"原位起飞",但它只有这一种动画——整库只做一件事。
- **B**:默认底色 `#fff`(Medium 白,页面即舞台);无任何 chrome、无 caption、无导航。
**克制到"查看器不存在"**:这就是它的设计立场。
- **C**:零信息层;`template` 选项可注入自定义布局(Facebook / Dropbox Paper 官方范例)——
证明"纯放大内核 + 自定义信息壳"是可行分层。
- **D**:无连拍导航(单图语义);`scrollOffset:40`:**滚动页面即关闭**,把"关"交给最顺手的手势。
- **E**:HD 模式 `data-zoom-src`:页内小图、开箱瞬间换高清(同宽高比前提),预览零等待。
- **F**:移动端点按开/点按关,无多余手势;无缩放(一档即满)。
### 3. Unsplash 照片页 ◐(curl 被 bot 墙拦截,未逐项复核)
- **A**:列表→详情是页面跳转非灯箱;大图页首屏即图主体,顶部细导航浮层滚动隐现。
- **B**:图外留白极小,信息全靠**悬浮层**:右侧竖排操作列(下载/尺寸选择/点赞/加合集/ⓘ),
不占图也不占底。
- **C**:ⓘ 召唤 Info 面板:相机/镜头/光圈/快门/ISO/焦距/发布时间/浏览数全量 EXIF;
平时全隐——"轻常驻 + 重召唤"的范本。下载默认给适配尺寸,原图藏在尺寸菜单。
- **D**:集合/相册内 ←→ 键连续浏览;相关照片网格殿后(发现回路)。
- **E**:渐进加载 + blur-up 占位是全站基建;L 键点赞的快捷键 affordance。
- **F**:移动端操作列收进底部栏,Info 面板变全屏抽屉。
### 4. 图虫 ◐(主站可达,查看器 JS 渲染未复核)
- **A**:信息流点击 → 全屏查看器直开(社区产品重内容直达,弱化过渡本身)。
- **B**:舞台全黑,chrome 依赖手势(边缘滑动)而非常驻按钮。
- **C**:摄影师视角分层:caption/描述 + **醒目完整 EXIF(机身/镜头/参数条)** + 作者卡(头像/
关注)+ 标签 + 社区数据(赞/藏)——信息密度最高,但靠 JS 分层收纳(常驻只留作者卡)。
- **D**:同组连拍左右滑 + 组内缩略导航;键盘支持弱于 PhotoSwipe/Flickr。
- **E**:加载有占位图(信息流即高清小图,开箱升级)。
- **F**:App 化手势(快滑翻页带跟手位移);双指缩放齐全。
- 教训面:信息全堆常驻会让暗房变"仪表盘"——我们只取它的 **EXIF 完整度**,不取密度。
### 5. Flickr(自选)✅ SSR 复核 + ◐ 视觉层
- **A**:照片 URL **直接开进 lightbox**(无中间页):暗房全屏沉浸,从网格进入有原位起飞感。
- **B**:舞台近纯黑(#212121 系);chrome 默认轻常驻:底部左侧 title/作者小卡(可隐),
其余全靠召唤。
- **C**:三段式:常驻小卡(title+作者)→ 右缘动作列(收藏/加画册/分享/下载尺寸/ⓘ)→
ⓘ 滑出右侧 EXIF 全量面板(机身/镜头/参数/软件/日期)。20 年沉淀的"常驻最轻、重的全召唤"。
- **D**:边缘 hover 箭头 + ←→/Esc/f/i 全键盘;f=收藏的键位文化。
- **E**:大图渐进加载;浏览数/评论在下折区(不抢舞台)。
- **F**:移动端 swipe 翻页、双指缩放、上滑收 chrome——三手势覆盖 95% 操作。
- 选它的理由:唯一同时满足"成熟产品 + SSR 可复核 + 键盘流 + 信息分层范本"。
## 二、差距矩阵(现状 v2 vs 天花板)
| 维度 | 我们现状(v2) | 天花板做法(出处) | 差距 |
|---|---|---|---|
| A 开合 | dialog 直开直关,零过渡 | 原位起飞+回落 333ms 单曲线(PS/MZ) | **大** |
| B 舞台 | backdrop .95 常驻;meta 恒占底条 | toggle-controls 沉浸(PS);轻常驻重召唤 | 中 |
| C 信息层 | 一层底条:caption+title+EXIF+动作平铺 | 双层:常驻轻卡 + 召唤 EXIF(FL/US);caption 就空放置(PS 插件) | 中 |
| D 导航 | ←→ 键/两侧钮/触摸滑动/loop ✅ | 同款 + spacing 换片节奏 + 放大态滑透翻页(PS) | 小 |
| E 微交互 | 切图 160ms 淡入;光标三态 ✅ | 相邻预载 [1,2]、2s 延迟加载指示、缩放档位语义(PS);HD 交换(MZ) | 中 |
| F 移动端 | 快滑翻页 + 双击缩放;**无双指缩放** | pinch 缩放/捏合关/竖拖关(PS)三件套 | **大** |
## 三、v2.1 增强提案(叠加式,不动 v2 引擎)
| # | 提案 | 做法 | 灰阶适配 | 工作量 | 优先级 |
|---|---|---|---|---|---|
| 1 | **原位起飞开合** | 点击缩略图:FLIP 动画双盒(外盒=裁切窗从缩略图矩形→舞台矩形,内图=cover 几何→contain 几何同步过渡,等价 PS `data-cropped` 路线);300ms `cubic-bezier(.4,0,.22,1)`(与站 `--ease` 同族);关闭回落到当前缩略图位置;reduced-motion 直开直关 | 起飞的是图不是 chrome;backdrop 照旧恒深 | 0.5–1 天 | **P0** |
| 2 | **背景点击关闭** | 点击落在图外区域即关(PS `bgClickAction:'close'` 同款),Esc 保留 | 无新增视觉 | 0.5h | **P0** |
| 3 | **chrome 双层淡入 + 召唤** | 开箱:层1(关闭/箭头/计数)+140ms、层2(caption/EXIF/动作)+300ms 淡入;`i` 键或图上单击 toggle 全隐(toggle-controls);zoom>1 自动隐、回 1x 淡回 | 淡入只有 opacity,无位移无色彩 | 0.5 天 | P1 |
| 4 | **相邻预载** | 开箱后 idle 预载 ±1 张 fullSrc(PS `preload` 精神,我们取 [1,1] 克制档) | 纯网络层 | 数行 | P1 |
| 5 | **双指缩放(移动端)** | pointer events 双指距离比→锚点缩放(复用 v2 `zoomAt` 数学);v2 已有 `touch-action:none` 地基 | 同 v2 | 0.5 天 | P1 |
| 6 | **换片过渡** | 切片:旧图 140ms 淡出、新图自侧向 10% 位移淡入(PS `spacing:.1` 节奏);v2 换图已回正缩放,天然兼容 | 仅 opacity/transform | 0.5 天 | P2 |
| 7 | **缩放指示 + 首开提示** | 缩放停稳后右下 90% 淡入 `168%` 900ms 淡出;首开箱 hint 胶囊"滚轮缩放 · 双击放大 · Esc 关闭"2.6s 自隐 | mono 小字,与 EXIF 同语言 | 数行 | P2 |
| 8 | **长图侧置 meta** | 竖图(ratio<0.9)且视口 ≥820px:caption+EXIF 移入侧 letterbox(PS dynamic-caption"就空放置");横图维持底部条 | 文字色不变,只挪位 | 0.5–1 天 | P2 |
| 9 | **HD 交换** | zoom>1.5x 懒换 `/photos-originals/` 原图,<1.5x 回 1440(MZ `data-zoom-src` 模式);404 兜底留 1440 | 纯数据层 | 0.5 天 | P2 |
| 10 | **移动竖拖关闭** | zoom=1 时竖向拖 >80px 关闭并回落缩略图(PS `closeOnVerticalDrag`) | 同关闭回落 | 0.5 天 | P2 |
落地注:生产保持原生 `<dialog>`(免费 focus-trap/returnFocus,PS 同款能力);起飞动画在 dialog 内
用 fixed 双盒实现,mockup 用 fixed 覆盖层纯为演示等价几何。
## 四、明确不做清单(与灰阶克制冲突)
- 粒子/彩光/视差/自动播放/轮播——舞台只属于照片
- backdrop blur——PhotoSwipe 文档明示重性能坑,且暗底已够沉浸
- 缩略图条(thumbstrip)——21 张规模用不上,常驻 chrome +1 与克制冲突(>50 张再议)
- 社交分享按钮——PS v5 自己都删了;博客无此回路
- tile deep-zoom / 万像素级缩放——仓内照片 2400px 封顶,用不到
- 全屏按钮——PS v5 已删(浏览器 F11 更顺);Esc + 沉浸足够
- 音效、进度条、弹跳物理回弹——动效纪律:全站唯一曲线
- chrome 染 accent 色——灯箱恒深恒灰,强调色止于站内链接
## 五、验证记录
- PhotoSwipe:photoswipe.com 七页 + unpkg v5.4.4 `photoswipe.esm.js` defaultOptions 源码核对(✅)
- medium-zoom:francoischalifour.com options/HD 指南 + GitHub README(✅)
- Unsplash:curl 被 bot 检测墙拦截,标记 ◐;图虫:详情页 JS 渲染不可静态复核,标记 ◐
- Flickr:照片页 SSR 抓取成功(photo-model/person-model JSON 在源码中),视觉层标 ◐
- mockup:单文件无依赖;图片引用 `../content/photos/` 仓内实拍(相对路径,需与仓库同置打开)
+394
View File
@@ -0,0 +1,394 @@
# 页面后台化方案:内容 / 布局 / 样式的可编辑边界
> 调研 + 提案,不改代码。基线 = 工作树 2026-11-02(含在途未提交改动)。
> 口径:所有结论落到本仓文件/字段;"能编辑什么"和"不能编辑什么"同等重要。
> **在途关系**:工作树里另有一条未提交车道(`git diff --stat`:home.yaml / opensource.yaml / projects.ts / site-content.ts / content.config.ts / ProjectWall.astro / index.astro / en-index / tokens.css / global.css,共 306 增)已落地「首页区块开关 + 开源页可见性 + README 开篇」。本文把它当**既有事实**评估,只补缺口与验收,不重复设计。
---
## 0. 现状盘点(文件级锚点)
| 关注点 | 现有实现 | 位置 |
|---|---|---|
| 集合/单例 schema | `siteHome` / `siteAbout` / `siteOpenSource` 三个 `glob(pattern 精确到单文件)` 单例 | `src/content.config.ts:78,114,119` |
| 后台字段 | `site_pages` 一集合三 files,字段与 schema 一一对应 | `public/admin/config.yml:110-260` |
| 文案访问层(含字段级回落) | `pick()/pickBi()` + `SITE_DEFAULTS` 逐字对齐历史文案 | `src/lib/site-content.ts:18-62` |
| markdown 渲染 | `createMarkdownProcessor()` **无参**(关键,见 §7) | `src/lib/site-content.ts:122-127` |
| 关于页结构 | h1 → intro(markdown) → `IdCard` → tracks(3 条硬编码) → build 段 | `src/components/pages/AboutPage.astro:1-70` |
| 关于页可变文案 | tracks/build 段全部来自 `t()` 字典(**非后台可改**) | `src/i18n/ui.ts:73-84` |
| 首页 | 结构化区块:IdCard/PostList/最新影像/Heatmap/ProjectWall/心跳 | `src/pages/index.astro`(zh/en 两份重复) |
| 开源页 | blurb + Heatmap + ProjectWall(limit 99) | `src/pages/opensource.astro` |
| 项目数据 | 构建期双源 fetch + 去重 + 过滤/排序 + 仓内 fallback JSON(现仅 1 条 `blog`) | `src/lib/projects.ts:24-134`、`src/data/projects-fallback.json` |
| README 开篇 | `GET /repos/wait4xx/wait4xx/readme`(Accept raw)+ `content/.github-readme.md` 缓存 | `src/lib/site-content.ts:204-256` |
| markdown 净化 | 仅文章管线挂了白名单(class/style/on*) | `astro.config.mjs:28`、`src/lib/rehype-class-whitelist.ts` |
| 设计 tokens | 全部 CSS 变量,明暗双套 | `src/styles/tokens.css`(124 行,含 dark 覆盖) |
**结论**:后台能力当前是「**平铺标量字段**」形态——能改文案与几个开关,不能改**结构、顺序、字段种类**。关于页的骨架(tracks/build 两段)完全在代码与 i18n 字典里,这正是用户"整页可编辑"诉求的断点。
---
## 1. 需求拆解(可验收能力清单)
| 编号 | 能力 | 现状 | 缺口 |
|---|---|---|---|
| C1 | 关于页**所有文案**可改(含 tracks/build 段) | intro 可改,其余在 `ui.ts` | 全部结构化区块未入 CMS |
| C2 | 关于页**结构可改**(增删段、调顺序、开关名片区) | 硬编码 | 无块模型 |
| C3 | 首页区块开关 / 条数 / 副标题 | 在途已实现(`sections.*`、`perTrackLimit`、`latestPhotos`、`subtitle`) | 缺"展示策略"类字段(见 §6) |
| C4 | 开源页"展示哪些项目" | 在途已实现(`mode/include/exclude/sort/perSource`) | 语义定稿 + 断言 + 现有 6 条截断副作用(见 §4) |
| C5 | 开源页开篇 = GitHub profile README | 在途已实现(`intro_from: github-readme`) | 缓存位置被 `git clean` 抹掉(见 §4.4) |
| C6 | **样式**可调(字体/字号/间距/灰阶/圆角) | 全在 `tokens.css` | 无数据层 |
| C7 | 双语 en 轨保真 | 字段级 `{zh,en}` + 回落 | 块模型需延续同规矩 |
| C8 | 构建期零运行时 + SEO/灰阶不回归 | 现状成立 | 需把"不回归"写成断言 |
---
## 2. 五方案对比
| | A 结构化单例字段扩展 | B 块构建器 | C 富文本整页 | D 样式即数据 | E 混合(推荐) |
|---|---|---|---|---|---|
| 能力上限 | 低:只能改预定义字段的值 | 高:增删/排序/差异化结构 | 最高:任意内容 | 中:只动感官参数 | 高(结构)+ 中(局部自由) |
| 破版风险 | 极低 | 低(渲染器受控) | **高**:作者可写出非设计语言结构 | 中(对比度/尺度) | 低-中 |
| XSS 面 | 0(无 HTML) | 小(组件渲染,URL/图片字段受控) | **大**:`set:html` 直通 | 0 | 小(唯一自由区走净化管线) |
| 编辑体验 | 顺手但僵硬;20 字段后 config.yml 成清单墙 | 顺手(新增时选块型、拖拽排序) | 最自由但无护栏 | 顺手(几个 color/number) | 顺手(块为主,自由区为辅) |
| 工作量 | 0.5d/页 | 3-4d(渲染器 + 9 类块 + 后台 types) | 0.5d(已有 intro 通道)但治不住质量 | 1d | 4-5d(含 B+D+净化) |
| 维护成本 | 每加一项改两处(yml+schema) | 每加块型改三处(yml+schema+组件),但**互不干扰** | 内容问题无代码兜底,靠人审 | 用户改坏颜色需回滚 git | 同 B,另加 token 白名单 |
| 对我们仓的结论 | **只适合开关/条数**(首页现状即其上限形态),**不作为关于页主路线** | 关于页主力 | **不作为主路线**(灰阶/双语/SEO 三条红线都靠人守) | 采纳为**独立一层**(受限白名单) | **采纳** |
补充结论(逐条):
- **A 的极限量化**:`public/admin/config.yml` 现已 260 行、覆盖 4 集合。关于页若走 A(名片/技能卡/时间线各 4-6 字段),预估 +60 行 yml、+40 行 schema、+30 行组件分支;且"某段删掉"只能靠 `enabled: false` 假装,顺序固定。→ 只保留给首页(首页是聚合页,区块种类稳定)。
- **B 的关键前提**:Sveltia 0.252 支持 **`list` 的 `types` + `typeKey`**(异构块),证据见 §3.4;无此能力则 B 退化成"同构对象列表"(每块字段全冗)。
- **C 的现实定位**:`about.yaml` 已有 markdown 整块通道(`intro`),可立刻扩成"多段 markdown 长文"。问题:长文无法表达 tracks 卡片(图标+双行)、无法控制段间节奏,且 raw HTML 通道当前**未净化**(§7.1)——直接放开等于开洞。
- **D 的边界**:可调项必须白名单化(约 12 个),不是整份 tokens.css;`--measure`(9 处引用)、`--accent`(38 处)属高放大倍率旋钮,见 §3.6。
---
## 3. 推荐方案 E:结构与落点
### 3.1 四层数据分层
| 层 | 载体 | 谁能改 | 例子 |
|---|---|---|---|
| L1 页面骨架 | `content/site/*.yaml` 的 `blocks: []` | 后台(增删排序) | 关于页整页 |
| L2 区块字段 | 每块 `type` 决定字段集(zod discriminatedUnion) | 后台 | prose / tracks / cards / timeline / quote / figure / linklist / cta / divider |
| L3 局部自由区 | `prose` / `richtext` 块内的 markdown(受白名单类 + tag 白名单) | 后台 | 一段带列表的说明 |
| L4 感官参数 | `content/site/theme.yaml` → 构建期生成 CSS 变量 | 后台(白名单旋钮) | 正文字号、圆角、强调色 |
### 3.2 yaml 结构总览(一眼看全)
```
content/site/
├── home.yaml # 已有:tagline/avatar/favicon/socials/subtitle/latestPhotos/perTrackLimit/sections/*
│ # 新增:heroStyle / projectsLimit / photosSource / postsTracks / announcement(markdown)
├── about.yaml # 已有:intro{zh,en}(保留为兼容字段,见 §5.5)
│ # 新增:layout{showIdCard,container} + blocks:[]
├── opensource.yaml # 已有(在途):blurb/blurb_after/repo_url/repo_label/pinned/mode/include/exclude/sort/perSource/intro_from
│ # 新增:projectsTitle / showHeatmap / intro_fallback_blurb(README 取不到时的专用文案)
└── theme.yaml # 新增:白名单感官参数(§3.6)
```
### 3.3 组件与读取层落点(写代码时照此建文件)
| 落点 | 职责 |
|---|---|
| `src/lib/page-blocks.ts`(新) | `getAboutBlocks(locale)`:读 `siteAbout` → 过滤 `enabled:false` → 未知 `type` 跳过 + `console.warn` → 文本字段 `pickBi` 回落 zh → 长度上限(块数 ≤ 24、单块文本 ≤ 4000 字) |
| `src/components/blocks/BlockRenderer.astro`(新) | `switch (block.type)` → 渲染子组件;统一外壳 `<section class="blk" data-block={block.type}>`(**data 属性是 E2E 断言的抓手**) |
| `src/components/blocks/{Prose,Tracks,Cards,Timeline,Quote,Figure,LinkList,Cta,Divider}.astro`(新) | 每类一个组件,样式从 `AboutPage.astro` 现状 `<style>` 段搬迁(`.sec-title`/`.track-row`/`.track-icon` 原样保留) |
| `src/components/pages/AboutPage.astro`(改) | 保留 `h1` + 可选 `IdCard`;`blocks` 为空 → `intro` markdown → 若再空则 `SITE_DEFAULTS.aboutIntro`(**零回归三级回落**) |
| `src/content.config.ts`(改) | `siteAbout` schema 扩展:`blocks` 用 `z.array(z.discriminatedUnion('type',[...]))`,图片字段用 `image()` |
| `public/admin/config.yml`(改) | `site_pages → about` 加 `blocks` 的 `list`+`types` 声明,`typeKey: type` |
| `src/lib/site-content.ts`(改) | `renderMarkdown` 补 rehype 插件(§7.2 一笔修两洞) |
| `src/lib/theme.ts`(新) | `getTheme()` + `themeVarsCss()` 生成 `:root:root{…}` 字符串 |
| `src/layouts/BaseLayout.astro`(改) | head 内联 `<style set:html={themeVarsCss()}>`(构建期静态,零运行时);`main` 容器档位由页面 `layout.container` 决定(`.container` / `.container-wide`,`global.css:43-44`) |
### 3.4 Sveltia 0.252 能力边界(本仓 bundle 实测取证)
证据文件:`public/admin/index.html`(pinned 本地 bundle 0.252.0,2.5MB)。
| 能力 | 结论 | 取证 |
|---|---|---|
| `list` 异构块(`types` + `typeKey`) | **支持**:新增条目时弹「Select List Type」,按 `name` 选型 | bundle 内 i18n 键 `select_list_type`、`list_type` 组件、`{types, typeKey='type'}` 解析路径 |
| 内建 widget 全集 | `boolean/color/code/compute/datetime/file/hidden/image/keyvalue/list/map/markdown/number/object/relation/richtext/select/string/text/uuid` | bundle 中 `ok=[…]` 常量 |
| `richtext`(所见即所得) | 支持,**落盘默认 markdown**(`field_defaults.richtext.format`,非 html) | bundle 中 `t===\`html\`?\`html\`:\`markdown\`` |
| 嵌套深度硬限制 | **未发现**:bundle 里唯一 `nestedDepth` 属 folder 集合路径深度,与字段嵌套无关 | 全量 grep |
| `list` 内再放 `list` | 类型表未排除,但**无保证** → 必须做一次 10 分钟后台实测冒烟(见 §8 P1 验收) | — |
| 拖拽排序 / 每块 summary | 支持(`summary` 模板 + reorder) | 现网 posts/moments 已在用 |
编辑体验判断:`blocks` 用 `types` 后,后台表现为「+ 添加 → 选块型 → 填该块字段」,列表可拖拽,每块可折叠。**代价**:字段标签必须写成中文短标签,否则块型选择弹窗会退化成英文 key。
### 3.5 双语策略(三条路线,选 A)
| 方案 | 做法 | 判断 |
|---|---|---|
| **A 字段级 `{zh,en}`(选)** | 每个文本字段仍是 `{zh,en}`,`en` 留空回落 `zh` | 与现状全站一致(`bilingual` schema + `pickBi`),后台一屏可见双语对照;缺点是块内字段数 ×2 |
| B 两套块数组(`blocks_zh`/`blocks_en`) | 结构可漂移 | **否决**:块数与顺序会分叉,维护地狱 |
| C en 构建期自动翻译 | 复用 `scripts/translate/` | **否决**:违反构建确定性(产物含未提交内容),且页面级译文无人校对 |
配套:`pickBi` 之外新增构建期**缺译清单**(§7.3 断言 #8),把"哪些 en 字段回落成了 zh"打成 WARN,避免静默全英文页变中文。
### 3.6 样式即数据(L4)边界表
| 允许(12 项) | 形态 | 风险控制 |
|---|---|---|
| `--accent` | color(明/暗两值) | 构建期算对比度(对 `--bg`)< 4.5 → 报警并回落默认(红线"全站唯一强调色"只能调色相,不能变多色) |
| `--radius-img` | number 4-16 | 钳制(08 文档裁定圆角上限 14) |
| `--text-h1/h2/h3` | number 22-32 | 单调性校验 h1>h2>h3,否则回落 |
| `--prose-size-reading/tech` | number 16-22 | 钳制 |
| `--leading-reading/leading-tech` | number 1.5-2.0 | 钳制 |
| `--space-3/4/5/6` | number 8-80 | 单调递增校验 |
| `--flow-p` / `--flow-h2-post` | number 0-2(em) | 钳制 |
| `--dur` | number 150-600(ms) | 钳制 |
| `--font-*` | select(内置四族,不含任意字体名) | **必须**枚举:任意字体名会让 614 个 @font-face 瘦身成果失效 |
**禁止**:`--measure`(9 处引用,改版心会连带 TOC/图库/首页栅格)、`--bp-*`(断点是媒体查询字面量)、`--code-bg/--code-ink`(Shiki `css-variables` 主题在 `astro.config.mjs` 之外耦合)、`--paper*`(书评纸墨是分轨身份)。
**注入方式**:`BaseLayout` head 内联 `<style>`,选择器用 `:root:root{…}`(0,2,0)与 `[data-theme="dark"]:root:root{…}`(0,3,0)压过 `tokens.css` 的 `:root`(0,1,0)/`[data-theme="dark"]`(0,1,0)——**不能靠 DOM 顺序**(`global.css` 的 `<link>` 注入位置与 inline `<style>` 的相对次序不可控)。断言:明暗两态 `getComputedStyle(html).getPropertyValue('--accent')` 都等于 yaml 值。
---
## 4. 开源页:项目可见性 + GitHub README
### 4.1 在途现状(已实现,不再设计,只定稿语义)
- 字段:`mode(all|only|exclude)` / `include[]` / `exclude[]` / `sort(pinned-first|stars|updated)` / `perSource(both|github|gitea)` / `pinned[]` / `intro_from(off|github-readme)`(`content/site/opensource.yaml`、`src/content.config.ts:128-138`、`src/lib/site-content.ts:180-201`)。
- 过滤实现:`perSource` → `mode` → `sort`,**不截断**,上限交给调用方(`src/lib/projects.ts:99-134`;这修掉了旧 `orderPinned` 内 `.slice(0,6)` 导致 `/opensource` 文案说"全部仓库"实际只显示 6 条的旧账)。
- 匹配键 = **仓库短名**(`name`),跨源同名先去重(同名优先 gitea 叙事,star 取 GitHub 真值,`src/lib/projects.ts:76-88`)。
### 4.2 语义定稿(必须写进后台 hint,否则用户会填错)
1. `mode: only` 时 **忽略** `exclude`;`mode: exclude` 时**忽略** `include`(v1 不做两者求交)。
2. `include/exclude` 填不存在的名字 = 静默无效(后台无下拉可选,因为仓库清单是构建期才拿到);→ 建议 P2 阶段把 `widget: string` 改成 `widget: select, options` 的**静态镜像**(yml 里手抄一份当前仓名)不划算,**v1 保持手填 + 构建期 WARN 打印未命中的名字**。
3. `sort: pinned-first` 仍是默认(首页要"置顶在前"叙事),`sort: stars|updated` 时 `pinned` 让位(现实现如此:`arrange` 里 stars/updated 分支直接 sort,不看 pinned)。
4. 首页固定 `limit=6`,开源页 `limit=99`(`ProjectWall.astro` props)。若仓库总数 >99 需调大,非阻塞。
### 4.3 是否用 GitHub profile README 作为开源页开篇 —— **可行,推荐按"默认关、用户开"上线**
| 评估项 | 结论 |
|---|---|
| 接口 | `GET /repos/wait4xx/wait4xx/readme` + `Accept: application/vnd.github.raw` = 一次请求返回 markdown 原文(现实现已如此,`site-content.ts:235-238`) |
| 限流 | 认证 5000 req/h;构建按 push 触发(webhook ~10 秒级),每天最多几十次 → 无压力。匿名 60 req/h 会更紧张,**必须走 token** |
| token | 复用 `~/docker_apps/blog/github-token.txt`(`src/lib/heatmap.ts:21` 同一份,0600,read 权限足够);**缺位是合法降级路径**,不报错只 WARN |
| CORS | **不涉及**:node 侧构建期 fetch,不是浏览器请求。(若哪天想做成客户端拉取,CORS 会直接堵死,且违反"构建期零运行时") |
| 许可 | 内容属本人 profile 仓(`wait4xx/wait4xx`),无第三方版权问题;README 若嵌第三方徽章图 = 外链图片,**构建期不下载、浏览器直连**,需接受 |
| 渲染质量 | README 常见 `<p align="center">`、徽章 `<img>`、`<details>`、表格、外链图片的**相对路径**(渲染到 `/opensource/` 下会 404)。现状只做了基础排版类(`opensource.astro` 的 `.readme-intro :global(...)` 段) |
| 破版风险 | 中:README 常含居中对齐/大标题/emoji 徽章,与"灰阶克制 + 版心 720px"张力大。→ 建议 **P5 做一次视觉复核**,必要时给 `.readme-intro` 加 `img{max-height}`、`h1{display:none}` 折叠 |
**缓存实现缺陷(必改)**:`README_CACHE = 'content/.github-readme.md'`(`site-content.ts:204`)写在仓内,而构建仓每次 `git fetch && reset --hard && git clean -fd`(`infra/deploy/build.sh:26`)会**抹掉未入仓缓存**——即"网络失败回落缓存"这层在生产构建里**永远命中不了**,实际回落链退化为"网络 → 无 → yaml blurb"。
修法(二选一,推荐后者):
- (a) 把缓存文件提交入仓,靠人工/定时提交更新 —— 引入"构建机写仓"脏状态,与"构建仓硬跟 origin/main"纪律冲突;
- (b) **移到仓外**:`~/docker_apps/blog/github-readme-cache.md`(与 token 同目录,天然持久,不被 clean 波及),TTL 24h(符合 GitHub API 缓存建议),并在 `writeFileSync` 失败时只 WARN。
### 4.4 失败回落矩阵(开源页)
| 失败点 | 现状行为 | 目标 |
|---|---|---|
| token 缺位 | WARN → 缓存 → null → 用 yaml blurb | 同(缓存改仓外后才有意义) |
| README 404 / 仓不存在 | WARN → 缓存 → blurb | 同 |
| 网络超时 | 现实现**无 AbortSignal 超时**(`projects.ts` 有 10s,README 没有)→ 挂起风险 | 补 `AbortSignal.timeout(8000)` |
| README 为空 | WARN → 缓存 → blurb | 同 |
| gitea 源整站 522(历史事故) | `Promise.allSettled` 单源失败不影响另一源 | 保持 |
| 双源全失败 | 仓内 `projects-fallback.json`(**现仅 1 条**) | 建议 P5 用一次真实构建产物回填 5-8 条 |
---
## 5. 关于页整页化
### 5.1 现有结构 → 区块映射(逐元素,无遗漏)
| 现有元素 | 文案来源 | 现状可否后台改 | 对应块型 |
|---|---|---|---|
| `<h1>关于` | `ui.ts:73` | ✗ | 页面 `title` 字段(或沿用字典) |
| intro 多段 markdown | `about.yaml intro.{zh,en}` | ✓ | **prose 块**(`intro` 字段保留为兼容回退) |
| 名片区(头像+名字+tagline+社交) | `IdCard` + `home.yaml` | 部分 ✓(tagline/socials 来自 home) | **layout.showIdCard 开关**(不拆成块:头像数据单一事实源在 home.yaml) |
| 「兴趣版图」标题 | `ui.ts:74` | ✗ | tracks 块的 `title` |
| 3 条 track(图标+名称+描述) | `ui.ts:75-80` | ✗ | tracks 块的 `items[]`(icon 键 + name/desc 双语) |
| 「本站如何构建」标题+正文 | `ui.ts:81-83` | ✗ | prose 块的 `title` + `body` |
→ **覆盖现有 About 只需 3 个块**(prose + tracks + prose)。其余块型是为"用户以后想加东西"准备的(§5.2)。
### 5.2 区块类型清单(v1 九类,够用且不失控)
| type | 字段(* = `{zh,en}`) | 说明 |
|---|---|---|
| `prose` | `title*?` `body*`(richtext/markdown) `width(narrow|full)` | 主力自由区(受 §7 净化) |
| `tracks` | `title*` `items[]{icon, name*, desc*}` | 覆盖现有兴趣版图;`icon` 走内置键白名单 |
| `cards` | `title*?` `columns(2|3)` `items[]{title*, desc*, href?, tag?}` | 技能/资源卡(复用 `.track-row` 视觉语言) |
| `timeline` | `title*?` `items[]{time, title*, desc*?}` | 经历/项目时间线(复用 `MomentsPage` 的 `.tl-*` 竖线语言) |
| `quote` | `text*` `cite?` | 复用 `.quote-line` + `--font-kai`(08 文档 D 金卡) |
| `figure` | `image`(image()) `caption*?` `alt` | 必须走 Astro 图片管线 |
| `linklist` | `title*?` `items[]{label*, href, note*?}` | 外链清单(比 markdown 裸链接可控) |
| `cta` | `text*` `href` `label*` | 单按钮行动点(克制:同屏最多 1 个) |
| `divider` | `style(line|space)` | 节奏控制(非装饰色彩) |
**不做**(放进 §9):`html` 块、`style` 块、`columns[grid]` 栅格构建器、任意 svg 块。
### 5.3 示例 yaml(直接照抄可跑通现有关于页)
```yaml
# content/site/about.yaml
layout:
showIdCard: true
container: narrow # narrow=.container(720) / wide=.container-wide(+200px)
# 以下为可选:不填则沿用标题「关于」
# title: { zh: 关于, en: About }
blocks:
- type: prose
title: { zh: '', en: '' } # 留空不渲染标题
width: narrow
body:
zh: |
写作者 · 读者 · 快门按压者,拥抱开源。
这里是我三条轨道的公开工作台:**读过的书**留下评注,**写过的代码**进项目墙,**按下的快门**收入图库。
en: |
Writer · Reader · Shutter-presser, embracing open source.
- type: tracks
title: { zh: 兴趣版图, en: Interests }
items:
- icon: book
name: { zh: 阅读, en: Reading }
desc: { zh: 文学与戏剧为主,读到哪写到哪。, en: 'Mostly literature and drama; I write as I read.' }
- icon: terminal
name: { zh: 技术, en: Tech }
desc: { zh: Linux、容器与自托管……, en: 'Linux, containers, and self-hosting — …' }
- icon: camera
name: { zh: 摄影, en: Photography }
desc: { zh: 街拍与山野……, en: 'Street and landscapes with a mirrorless camera, …' }
- type: prose
title: { zh: 本站如何构建, en: How this site is built }
body:
zh: 静态生成,构建即发布;字体自托管,评论自托管,全文索引在构建期生成。
en: Statically generated, publish-on-build; self-hosted fonts, comments, and a build-time search index.
```
`icon` 键白名单(代码侧注册,同 `SocialLinks` 纪律 `src/components/SocialLinks.astro:7-19`):`book / terminal / camera / code / git / rss / email / github / bilibili / x / star / link`。后台填表外的键 → 渲染纯文字(不破版)。
### 5.4 视觉保留(不动设计体系)
- 类名与样式从 `AboutPage.astro:70-143` **原样搬迁**到 `src/components/blocks/*.astro`:`.sec-title`(mono 14px/ink-3/0.08em)、`.track-row`(1px `--line` + radius 10px + `--space-3/4` 内距)、`.track-icon`(36×36 / `--bg-soft`)。
- 新增块**只允许**用既有 token;新组件不得引入新颜色字面量(用 `--ink-*`/`--line`/`--bg-soft`)。这条要在评审时按 `grep '#[0-9a-fA-F]\{6\}' src/components/blocks/` 结果把关(`ProjectWall` 的语言色点是既存例外,不得扩散)。
- 关于页 `--measure` 版心不变(prose 块 `max-width: var(--measure)`,`AboutPage.astro:85`)。
### 5.5 兼容与零回归
- `intro` 字段**不删**:`getAboutBlocks()` 见 `blocks` 缺省/空数组 → 走旧路径(`intro` markdown → 默认文案)。这样 Sveltia 里手滑清空 blocks 不会白页。
- `t('about_track_*')` 等字典键**不删**:作为 `SITE_DEFAULTS` 的英文默认源;字典继续服务"没有 blocks 的旧数据"。
- zh/en 双路由共用 `AboutPage.astro`(`src/pages/about.astro`、`src/pages/en/about.astro`),**块数据只存一份**,locale 在读取层选字段 —— 这是 en 轨不漂移的结构保证。
---
## 6. 首页扩展(结构化,不上 blocks)
理由:首页是**聚合页**(列表/影像/热力图/项目墙都是"取集合切片"),块构建器在这里只会增加"选块型 → 填聚合配置"的绕路,且首页 zh/en 是两份重复文件(`src/pages/index.astro` vs `src/pages/en/index.astro`),更适合"字段少而稳"。
| 已有(在途) | 建议补 | 字段形态 | 落地 |
|---|---|---|---|
| `tagline` `subtitle` | — | `{zh,en}` | `site-content.ts:160-175` |
| `sections{tracks,photos,heatmap,projects}` | `heartbeat`(心跳行开关) | boolean | 同上 |
| `perTrackLimit`(4-8) | `postsTracks`(显示哪几条轨,默认 reading+tech) | select multiple | 需给 `PostList` 加 `tracks` prop |
| `latestPhotos`(0-6) | `photosSource(curated|latest)`:curated 读 `photos` 集合的 `pinned` 标记 | select | 需给 photos schema 加 `pinned` |
| — | `heroStyle(idcard|plain)` | select | `IdCard` 加 variant |
| — | `projectsLimit`(0-8,0=隐藏) | number | 传给 `ProjectWall limit` |
| — | `announcement`(一段 markdown 短公告,渲染在 tagline 下) | `{zh,en}` markdown | 复用 `renderMarkdown`(须走净化) |
**风险**:`index.astro` 与 `en/index.astro` 是**手抄双份**,任何新字段必须在两处同时接;建议 P4 顺手抽出 `src/components/pages/HomePage.astro`(吃 `locale` prop),双页变薄壳 —— 这是"双语不漂移"的结构性修法,先于新字段做。
---
## 7. 安全与构建可靠性
### 7.1 现状漏洞(三条,均已定位到行)
1. **白名单对页面文案不生效**:`renderMarkdown()` 调 `createMarkdownProcessor()` **无参**(`src/lib/site-content.ts:124`),`@astrojs/markdown-remark` 用默认 `rehypePlugins: []`(`node_modules/.pnpm/@astrojs+internal-helpers@0.13.0/.../markdown.js:21`),且默认 `allowDangerousHtml: true` + 结尾无条件挂 `rehype-raw`(`@astrojs/markdown-remark/dist/index.js:53-56,80`)。→ **`about.yaml` 的 raw HTML 直通 `set:html`**(`AboutPage.astro:20`),`<script>`/`style`/`on*` 全不被剥。文章管线有白名单(`astro.config.mjs:28`),页面文案管线**没有**。
2. **README 是远程内容**:`githubReadmeMarkdown()` 把网上下来的 markdown 直接进 `set:html`(`src/pages/opensource.astro:17`)。即便 README 属本人仓,也等于把"远程内容"当可信 HTML 处理。
3. **`style` 属性会用内联样式破灰阶**:现白名单剥 `style`,但只对文章生效;块模型若不统一走净化管线,同样的洞会出现在关于页。
### 7.2 三道防线(按顺序实施)
**D1 修管线(一处修两洞,P0)**
```ts
// src/lib/site-content.ts
const { createMarkdownProcessor } = await import('@astrojs/markdown-remark');
mdProcessor ??= createMarkdownProcessor({
rehypePlugins: [rehypeRaw, rehypeClassWhitelist, rehypeLinkCards], // 与 astro.config.mjs:28 同序
});
```
副作用(正向):页面 markdown 里的独立成段链接**也会变成 OG 卡片**(`rehypeLinkCards` 现已带 `content/.link-cache.json` 缓存),这是"免费能力",需在写作指南里说明。
**D2 加 tag 级允许清单(P0,扩展 `src/lib/rehype-class-whitelist.ts` 或新建 `rehype-content-sanitize.ts`)**
- 允许标签:`p br strong em del code pre blockquote ul ol li a img h2 h3 h4 hr table thead tbody tr th td figure figcaption span div`(**不含** `script style iframe object embed form input link meta svg`)。
- 属性允许:`a[href,title,rel]`、`img[src,alt,width,height,loading]`、其余仅 `class`(白名单)+ `id`(heading 锚)。
- 协议允许:`http: https: mailto:` + 站内相对路径;`javascript:`/`data:`(除 `image/*` 小图外)一律剥。
- 未知块 `type` → 跳过 + WARN,**不炸构建**。
**D3 图片必须走管线(P1,随块模型)**
- 块内 `image` 字段声明为 `image()`(Astro helper),媒体目录 `content/site/assets`(`public_folder: ./assets`,与 avatar 现状一致,`config.yml:117-118`)。
- 禁止在块里填 `/media/...` 或外链图(**schema 层拦**:`z.string().refine(s => !/^https?:/.test(s))`,只放行 `image()` 类型)。体积纪律沿用写作指南(单张 ≤1MB)。
### 7.3 验收断言清单(可直接写成 Playwright / grep 断言)
| # | 断言 | 手段 |
|---|---|---|
| 1 | 关于页块数与顺序 == yaml(含 `enabled:false` 被滤掉) | `page.locator('[data-block]')` 逐个取 `data-block` 属性拼串比对 |
| 2 | 零配置零回归:`about.yaml` 只有 `intro` 时,`/about/` 与改动前快照一致 | 旧产物 dist 对比(`diff <(grep -o 'class="[^"]*"' old) new`) |
| 3 | 注入负向测试:`intro` 写 `<script>alert(1)</script>` 与 `<span style="color:red">` → 构建后 dist 不含 `alert(1)`、不含 `style="color` | `grep -c` 必须为 0 |
| 4 | 类白名单:`<span class="font-kai foo">` → 只留 `font-kai` | 同上 grep |
| 5 | 未知块型不炸:yaml 写 `type: not-a-block` → 构建成功 + stderr 含 WARN + 页面无该块 | 手跑一次 build |
| 6 | 缺译可见:`en` 全空的页面 → 渲染 zh 文案 + 构建 WARN 列出字段路径 | 静态检查 build 输出 |
| 7 | tokens 生效与压过:明暗两态 `--accent` == yaml 值;`--radius-img` 同 | `getComputedStyle(document.documentElement).getPropertyValue('--accent')` |
| 8 | 对比度闸门:token 化颜色对 `--bg` ≥4.5:1(含暗色态) | 构建期脚本 `scripts/build-perf/contrast-check.mjs`(相对亮度公式,08 文档同口径) |
| 9 | 图片走管线:dist 中关于页图片 `src` 含 `/_astro/` 且带 `width/height` | grep |
| 10 | 项目墙可见性:`mode: only, include: [blog, pi-board]` → dist `/opensource/` 的 `.repo-name` 恰 2 条;`mode: exclude` 同理 | Playwright 计数 |
| 11 | README 回落:把 token 文件改名后构建 → `/opensource/` 仍出开篇(缓存或 blurb),构建 exit 0 | 手工一次 |
| 12 | 构建期零运行时:`dist/about/index.html` 新增 inline `<script>` 数为 0(相对基线) | grep 计数对比 |
### 7.4 失败回落纪律(写进读取层,不散在组件里)
| 数据 | 缺省/损坏时 | 目标 |
|---|---|---|
| `blocks` 空/缺 | → `intro` → `SITE_DEFAULTS.aboutIntro` | 页面永不空 |
| 某块缺必填字段 | zod 报错即构建失败(**块内必填**要谨慎:`title` 设 optional,`body` 必填) | 宁可构建失败也不出半块 |
| 块文本超长 | 读取层截断 + WARN | 防"一顿贴整篇长文" |
| theme.yaml 非法值 | 钳制/回落默认 + WARN | 绝不让一个 `#zzz` 拖垮构建 |
| 项目 API 全挂 | fallback JSON | 已有 |
| README 全挂 | 仓外缓存 → yaml blurb | §4.4 |
---
## 8. 分阶段实施清单
| 阶段 | 内容 | 工作量 | 依赖 | 落地文件 | 验收 |
|---|---|---|---|---|---|
| **P0** | 修 markdown 管线(D1)+ tag 白名单(D2)+ 负向断言 #3/#4 | 0.5d | 无 | `src/lib/site-content.ts`、`src/lib/rehype-content-sanitize.ts`(新)、`tests/e2e.mjs` 或 `scripts/build-perf/content-lint.mjs` | 断言 3/4 绿,现有文章渲染零回归 |
| **P0.5** | 后台实测冒烟:`list` 内嵌 `list`、`richtext` 嵌套在 `object` 内是否可用 | 0.2d | 无(需 token 登录一次) | 无代码,结论回写本文 §3.4 | 能新增/拖拽/保存并构建通过 |
| **P1** | blocks 骨架:schema + `page-blocks.ts` + `BlockRenderer` + prose/tracks/divider 三块 + 后台 `types` 声明 | 2d | P0、P0.5 | `src/content.config.ts`、`src/lib/page-blocks.ts`、`src/components/blocks/*`、`src/components/pages/AboutPage.astro`、`public/admin/config.yml` | 断言 1/2/5/6 |
| **P2** | 块型扩充(cards/timeline/quote/figure/linklist/cta)+ 图标键白名单 + 图片走管线(D3) | 1.5d | P1 | 同上 + `content/site/assets/` | 断言 9;视觉复核一轮(glm-5.3-flash) |
| **P3** | theme.yaml + `theme.ts` + BaseLayout 内联变量 + 对比度闸门脚本 | 1d | P0(净化不相关,可并行) | `content/site/theme.yaml`、`src/lib/theme.ts`、`src/layouts/BaseLayout.astro`、`scripts/build-perf/contrast-check.mjs`、`public/admin/config.yml` | 断言 7/8 |
| **P4** | 首页字段细化 + 抽 `HomePage.astro` 消双份 | 1d | P0 | `src/components/pages/HomePage.astro`(新)、`src/pages/index.astro`、`src/pages/en/index.astro`、`site-content.ts` | zh/en 区块数一致;开关组合抽测 |
| **P5** | 开源页收尾:README 缓存移仓外 + 超时 + fallback JSON 回填 5-8 条 + `include` 未命中 WARN | 0.5d | 在途车道合并 | `src/lib/site-content.ts`、`src/data/projects-fallback.json` | 断言 10/11 |
| **P6** | 文档:`docs/管理台.md`(块编辑怎么用)、`docs/写作指南.md`(受限类 + 块字段)、`public/admin/config.yml` hint 全量中文 | 0.5d | P1-P4 | 三文件 | 后台每字段都有中文 hint |
合计 ≈7.2d(不含视觉复核排队)。**建议顺序**:P0 → P0.5 →(P1 与 P3 可并行)→ P2 → P5 → P4 → P6。P5 最便宜且能立刻消掉在途车道的生产隐患,可提前插队。
---
## 9. 明确不做(附理由)
1. **任意 HTML/`style` 块**:等于把灰阶骨架与 XSS 面交给后台输入;08 文档红线(禁渐变/玻璃/大圆角)无法用代码守住。
2. **可视化栅格/自由拖拽布局(columns、grid 编辑器)**:Astro 侧要一套运行时布局引擎,违"构建期零运行时";且移动端塌陷规则无法交给后台。
3. **可编辑颜色集(多个强调色/任意 hex 用于文本)**:全站唯一强调色是丙路线根基(`tokens.css:21-22`),只允许 `--accent` 一个旋钮并过对比度闸门。
4. **`--measure` / 断点可调**:`--measure` 9 处引用、断点写死在媒体查询里;改版心会连带 TOC/栅格/图库。
5. **任意字体名输入**:字体子集化(`scripts/build-perf/subset-font-css.mjs`)依赖固定四族,自由输入会静默失效或拖大 CSS。
6. **块模型用于首页**:首页是聚合页(§6),结构化字段更稳、更好审。
7. **`blocks_zh`/`blocks_en` 双数组**:结构会分叉。
8. **构建期自动翻译页面文案**:破坏构建确定性(产物含未提交内容),且无人校对。译文必须入仓(沿用 `posts-en` 的 `locked: true` 纪律)。
9. **运行时/客户端渲染页面内容**(含客户端拉 GitHub README):CORS、SEO、性能三杀。
10. **后台新建任意页面路由(v1 不做)**:需 `[...slug].astro` + 导航/站点地图/双语配对/`hidden` 语义,收益低于成本;等关于页验证过一轮再议(那时复用同一套 BlockRenderer,成本会低很多)。
11. **换 CMS / 上 Netlify 式可视化编辑器**:现有 Sveltia 0.252 已支持 `types` 异构列表(§3.4),够用;迁移成本远高于收益。
12. **每页独立 tokens**(页面级主题覆盖):双层变量来源会让"某页为什么和别的页不一样"变成排查噩梦;v1 只有全局一套。
---
## 附:待用户拍板的三个岔路
1. **关于页块模型 vs 长文**:块模型(本文推荐,结构可控、视觉一致)vs 单一 markdown 长文(改起来最快,但 tracks 卡片做不出来)。
2. **README 开篇是否开**:开(省手工维护,但版式受 GitHub README 影响)vs 关(`intro_from: "off"`,全手控)。
3. **样式旋钮范围**:只给 3 个(accent/radius/字号)还是一整套 12 个(§3.6)——旋钮越多越需要对比度闸门与视觉复核,建议先给 3 个、跑稳一版再扩。
+25
View File
@@ -36,3 +36,28 @@ sudo bash infra/deploy/rollback.sh --exec # 秒级回 Halo
- 新文章工作流:本地或 gitea web 写 md → push main → webhook 自动构建发布 - 新文章工作流:本地或 gitea web 写 md → push main → webhook 自动构建发布
- 新照片:上传 content/photos/ 后跑 `python3 scripts/photos_ingest.py` 再 commit - 新照片:上传 content/photos/ 后跑 `python3 scripts/photos_ingest.py` 再 commit
- 翻译:新文章 push 后跑 `python3 scripts/translate/translate_posts.py`(幂等) - 翻译:新文章 push 后跑 `python3 scripts/translate/translate_posts.py`(幂等)
---
## SEO 解封操作单(Phase 5f T1,2026-10-26 立案)
**背景**:生产全站自上线起带 `X-Robots-Tag: noindex`(/etc/nginx/snippets/esa-origin-ssl.conf:11 共享片段误伤博客 vhost)+ robots.txt 404 + ESA 将带 noindex 的响应头缓存 30 天——搜索引擎全站不可见。
**第一步:nginx(需 sudo)**
1. 编辑 `/etc/nginx/snippets/esa-origin-ssl.conf`:删除第 11 行 `add_header X-Robots-Tag noindex always;`(改前备份 .bak)
2. 给确实需要 noindex 的内部服务 vhost(api/oj/token/fc/latex/cpa/git)各 server 块补回该行
3. 博客 vhost 补局部:`location /admin/ { add_header X-Robots-Tag noindex always; }`
4. `sudo nginx -t && sudo systemctl reload nginx`
**第二步:ESA 控制台(不做则 noindex 再活 30 天)**
1. 全站 purge `wait4x.online`
2. HTML 边缘 TTL 调 ≤1h(当前 2592000s,靠回源重验证更新——头会固化)
3. 加规则:`/artalk/api/*` 绕过缓存(GET stats 有 Age 头实锤,pv 数字会冻)
**第三步:验证**
```bash
curl -sI https://wait4x.online/ | grep -ci x-robots-tag # 期望 0
curl -s https://wait4x.online/robots.txt | head -3 # 期望 User-agent + Sitemap
curl -sI https://wait4x.online/admin/ | grep -ci noindex # 期望 1(局部保留)
```
仓库侧已就绪:public/robots.txt(本提交);生产 Lighthouse SEO≥95 复测随后入档。
+383 -2
View File
@@ -2,6 +2,36 @@
> 适用:`content/posts/*.md`(书评与技术轨通用)。资产目录约定 + 三种媒体的写法 + 体积纪律。 > 适用:`content/posts/*.md`(书评与技术轨通用)。资产目录约定 + 三种媒体的写法 + 体积纪律。
## 写作模板(从这里开始,2026-11-02 起)
别从空白文件起步——**复制现成的模板**:
```bash
cp content/posts/writing-template.md content/posts/my-new-post.md
# 改 frontmatter 的 title / pubDate / tags / description,删掉不需要的段落
```
模板本身是一篇真文章(线上读:`/archives/writing-template/`),每节都在跑一种能力,你看到的效果就是源码的效果:标题层级、彩色强调、字族切换、引用、列表、代码窗条、表格、图片放大、链接卡片、`<details>` 折叠、分隔线。
写之前只需记住三条纪律:
- **彩色不限次数**(2026-11-02 用户拍板取消原来的"一页最多三处"):正文想用多少处颜色都可以;仍守的是另一条——**外壳灰阶**(导航/页脚/按钮/背景不染色),因为外壳是气质,正文是表达
- **整页换字族要谨慎**:会给访问者拉一份中文字体(汇文明朝整页约 420KB,已子集化)
- **折叠放细节不放结论**:`<details>` 里的内容可能永远没人展开
## 置顶文章
首页、归档、书架三处列表默认按日期倒序。想让某篇长期排在最前,frontmatter 加一行:
```yaml
pinned: true
```
语义边界(刻意设计成这样):
- **只影响列表顺序**,不改 `pubDate`、不改 URL、不影响 RSS 的时间序列(订阅者不会看到时间错乱)
- 多篇同时置顶时,它们内部仍按日期倒序
- 列表里会显示一个灰阶「置顶」小徽标,取消置顶后立刻回到时间序
- 后台「文章」编辑页有「置顶」开关,不必手改 frontmatter
## 目录约定 ## 目录约定
``` ```
@@ -22,10 +52,11 @@ content/posts/
```markdown ```markdown
![外滩夜景](./assets/my-post/bund.jpg) ![外滩夜景](./assets/my-post/bund.jpg)
*图:外滩的傍晚* ← 紧跟媒体的斜体独立段 = 图注(居中小字) <p class="fig-cap">图:外滩的傍晚</p> ← 显式图注类(居中小字)
``` ```
- 图注可选;写法固定为 `*…*` 单独成段,紧跟图片 - 图注可选;**必须用 `<p class="fig-cap">…</p>`**(2026-11-02 起)
- ⚠️ 旧写法 `*斜体独立段*` 已不再自动当图注:此前它会把正文里所有独占成段的斜体都变成块级居中(用户实测的排版 bug),现在斜体恢复为正常行内强调
- 压缩纪律:入仓前长边 ≤2400px(可跑 `python3 scripts/photos_ingest.py`,对 posts 资产同样有效的思路,图片请自行用同规格压缩) - 压缩纪律:入仓前长边 ≤2400px(可跑 `python3 scripts/photos_ingest.py`,对 posts 资产同样有效的思路,图片请自行用同规格压缩)
## 视频 / 音频 ## 视频 / 音频
@@ -65,7 +96,357 @@ public/media/my-post/clip.mp4 ← 文件放这里
见 `content/posts/media-sample.md`(draft 文章,仅构建验证用,不上线)。 见 `content/posts/media-sample.md`(draft 文章,仅构建验证用,不上线)。
## 灯箱说明文字(图库 caption)
图库照片的 `caption` 字段走同一套富文本管线:markdown(`**加粗**`、换行)+ 排版类(`<span class="c-blue font-kai">`)。灯箱里深底上的彩色会自动取暗底档。
图库灯箱关闭:右上 `×`、`Esc`、点背景、点图(1× 时)都能关;缩放后移动鼠标召唤控件。
## 链接卡片(OG 预览卡)
**一行只放一个链接、且它独立成段**时,构建期会抓取该页的 OG 信息渲染成卡片(标题 / 描述 / 站点图标 / 可选缩略图):
```markdown
https://astro.build/
[也可以是 markdown 链接形态](https://astro.build/)
```
规则与边界:
- **内联链接不变**:夹在句子里的链接保持普通样式(`这是一句带 [链接](https://example.com) 的话` → 不会变卡),所以不会把整篇变成卡片墙
- **抓取失败一律回落普通链接**:404、无 OG、超时(4s)、站点拒爬都不影响构建
- **每篇最多 10 张卡**,超出的回落普通链接(防止文章变卡片流)
- **缓存**:结果存 `content/.link-cache.json`(入仓,保证构建确定性);条目 30 天后自动重抓。想强制刷新某条——删掉该 URL 的条目即可
- 缩略图取 `og:image`,无图不占位;图标取站点而非第三方 favicon 服务
## 文章排版类(受限白名单)
正文里可用 `<span class="…">` / `<p class="…">` 做**受限**排版。构建期有白名单过滤:**只放行下表类名,其它 class 与 `style`、`on*` 属性一律被剥离**(防注入 + 保护灰阶骨架)。
### 灰阶层级类(全部实测亮暗对比度 ≥4.5:1)
| 类名 | 作用 |
|---|---|
| `ink-1` / `ink-2` / `ink-3` | 指定灰阶层(标题/正文/次要) |
| `ink-muted` | 最淡灰(注释感) |
| `ink-faint` | 最淡**可读**档(再淡即不达 AA) |
| `ink-warm` | 暖调正文(书评气质) |
| `ink-cool` | 冷调正文(技术气质) |
| `ink-accent` | 强调(链接色,少量点缀) |
| `ink-accent-soft` | 弱强调(比强调淡一档) |
### 语义色板(8 色相 × 标准/soft;亮暗各自取值,实测全部 ≥4.5:1)
| 色相 | 标准类 | soft 类 | 亮态对比度(标准 / soft) | 暗态(标准 / soft) |
|---|---|---|---|---|
| 蓝 | `c-blue` | `c-blue-soft` | 6.46 / 4.58 | 7.70 / 4.61 |
| 青 | `c-teal` | `c-teal-soft` | 5.00 / 4.57 | 9.77 / 4.52 |
| 绿 | `c-green` | `c-green-soft` | 5.38 / 4.67 | 10.38 / 4.67 |
| 黄 | `c-yellow` | `c-yellow-soft` | 6.45 / 4.57 | 11.73 / 4.52 |
| 橙 | `c-orange` | `c-orange-soft` | 6.26 / 4.65 | 9.44 / 4.58 |
| 红 | `c-red` | `c-red-soft` | 6.68 / 4.55 | 7.51 / 4.50 |
| 粉 | `c-pink` | `c-pink-soft` | 6.96 / 4.73 | 8.87 / 4.52 |
| 紫 | `c-purple` | `c-purple-soft` | 7.46 / 4.53 | 8.25 / 4.64 |
- 黄色在亮态被压暗到 `#7a5900`(原色在白底只有 ~1.8:1),这是**必须**的:彩色文字要能读
- `soft` 档是"阈值内最淡值",适合次要强调;标准档用于关键强调
> **使用纪律(重要)**:彩色**只作用于文章正文的强调文字**。站点外壳(导航、页脚、按钮、背景、卡片边框)一律保持灰阶——整站不因正文用色而花掉。写作时一个页面里的彩色强调建议不超过 3 处。
### 字体类
| 类名 | 作用 | 字体 | 许可证 | 加载 |
|---|---|---|---|---|
| `font-serif` | 书评正文族 | Noto Serif SC | OFL-1.1 | 自托管 |
| `font-kai` | 金句族 | 霞鹜文楷 Screen | OFL-1.1 | 自托管 |
| `font-sans` | UI/技术族 | Noto Sans SC | OFL-1.1 | 自托管 |
| `font-mono` | 标签/数值 | JetBrains Mono | OFL-1.1 | 自托管 |
| `font-song` | 宋体族 | Noto Serif SC 优先,系统宋体兜底 | OFL-1.1 | **自托管优先** |
| `font-kai-sys` | 楷体族 | 霞鹜文楷优先,系统楷体兜底 | OFL-1.1 | **自托管优先** |
| `font-hei-sys` | 黑体族 | Noto Sans SC 优先,系统黑体兜底 | OFL-1.1 | **自托管优先** |
| `font-fangsong` | 仿宋族 | FangSong/STFangsong | — | ⚠️ **纯系统回退**(暂无 OFL 自托管仿宋包) |
### 文学排版类(段落级)
| 类名 | 作用 |
|---|---|
| `drop-cap` | 段落首字下沉(书评开篇) |
| `quote-line` | 金句块(左侧竖线 + 文楷) |
| `fig-cap` | 图注(居中小字) |
### 首批字体(已落地,6 族 + 3 档自托管优先)
| 类名 | 字体 | 许可证 | webfont 嵌入 | 用到该字体的页面增量 |
|---|---|---|---|---|
| `font-serif` | Noto Serif SC | OFL-1.1 | ✅ OFL 允许 | 基线内 |
| `font-sans` | Noto Sans SC | OFL-1.1 | ✅ | 基线内 |
| `font-kai` | 霞鹜文楷 Screen | OFL-1.1 | ✅ | 基线内 |
| `font-mono` | JetBrains Mono | OFL-1.1 | ✅ | 基线内 |
| `font-huiwen` | 汇文明朝体(文风切片包 339 片) | OFL-1.1 | ✅ | **535KB** |
| `font-kuaile` | 站酷快乐体 | OFL-1.1 | ✅ | **163KB** |
| `font-song` / `font-kai-sys` / `font-hei-sys` | 上述自托管字体优先,系统字族兜底 | OFL-1.1 | ✅ | **+0KB** |
体积账结论(2026-11-02 实测,本地无 gzip 的相对值):**未使用某字体的页面,字体字节增量恒为 0**(构建期按页子集化 + unicode-range 按需拉取)。汇文采用文风开源的 cn-font-split 切片包(339 片),实测只拉用到字符的 19 片。
**再裁一刀(按本站实际用字子集化)**:切片包本身粒度粗(一片含数百字),只做标题/短句的艺术字体往往只用到几十个字符。`scripts/build-perf/subset-fonts.py` 读字体 CSS 的 `@font-face`、收集内容里真实出现的字符、逐片求交集后用 `pyftsubset` 重写——实测汇文整页 **694KB → 420KB**(约 −40%)。命令:
```bash
python3 scripts/build-perf/subset-fonts.py \
--css node_modules/@fontsource/huiwen-mincho/index.css \
--out public/fonts/subset/huiwen \
--chars-from content/posts content/moments content/site/*.yaml
# 可选:--chars-mode all|headings(只统计标题字)、--min-bytes 4096(小于该值的片不裁)
```
子集化产物输出到 `--out` 指定目录(如 `public/fonts/subset/`);把它接进站点(构建链挂守卫 + 字体 CSS 指向子集)是独立一步,接线后随构建自动生成,未接线前需手动跑。**换字/加字后重跑一次**,否则新字会回落到下一个切片或系统字体。
### 5i 字族扩展六款(2026-11-03,全站共 12 族)
| 类名 | 字体 | 适用场景 | 许可证 / 来源 | 切片 | 单页增量(实测) |
|---|---|---|---|---|---|
| `font-xiaowei` | 站酷小薇 ZCOOL XiaoWei | 文学味标题、书摘 | OFL-1.1 / google/fonts `ofl/zcoolxiaowei` | 100 片 | 44.9KB |
| `font-huangyou` | 站酷庆科黄油体 | 圆润标题、栏目名 | OFL-1.1 / `ofl/zcoolqingkehuangyou` | 100 片 | 27.1KB |
| `font-mashanzheng` | 马善政毛笔楷书 | 书法短句、题记 | OFL-1.1 / `ofl/mashanzheng` | 100 片 | 45.7KB |
| `font-liujianmaocao` | 柳建毛草 | 草书点缀(只宜 1-2 字) | OFL-1.1 / `ofl/liujianmaocao` | 100 片 | 30.0KB |
| `font-smiley` | 得意黑 Smiley Sans(Oblique) | 现代倾斜标题、强调句 | OFL-1.1 / `atelier-anchor/smiley-sans` v2.0.1 | 100 片 | 27.1KB |
| `font-neoxihei` | 霞鹜新晰黑 LXGW Neo XiHei | 比思源黑更清晰的正文黑体 | OFL / `lxgw/LxgwNeoXiHei` | 96 片 | 30.0KB |
- 六款均按**站内用字**细切(约 19 字/片,单片 2–12KB),且只在内容里写到对应类时才加载(探测在 `src/lib/font-needs.ts`)——首页、归档列表这类无字体类的页面**零增量**。
- **覆盖边界**:切片只含站内出现过的字(当前约 1900 字)。新写了字典外的新字(尤其生僻字)要先重跑切片,否则该字回落系统字族。
- **重切命令**(字体源文件不入仓,按需从上面的来源下载):
```bash
python3 scripts/build-perf/subset-fonts.py \
--split-ttf /path/to/YourFont.ttf --family "Font Family Name" \
--slices 100 --order-from public/fonts/subset/hwmc/fonts.css \
--out public/fonts/<shortname> \
--chars-from 'content/posts/*.md' 'content/site/*.yaml' 'content/moments/*.md'
```
- **体积闸门**:单页增量 >150KB 必须子集化或降级。本次六款实测 27–46KB(单句场景)全部达标;@fontsource 官方中文包切片粒度约 33 字/片、单片 40KB+,一句话就 300–440KB/页,**已弃用**(改从原始 TTF 细切)。
### 候补字体清单(未落地,加一款约"一条命令 + 一次体积账")
| 字体 | 许可证 | 允许 webfont 嵌入 | 未落地原因 |
|---|---|---|---|
| HarmonyOS Sans | 华为字体许可(免费商用) | ✅ 明文允许"嵌入、捆绑、重新分发",需标注 | npm 无包,官网整包 52MB |
| MiSans | 小米字体许可(免费商用) | ✅ 官方 FAQ 明确 "embedded fonts: Yes",需注明 | npm 无包 |
| OPPO Sans | 免费授权(含商用) | ✅(附带"不向他方提供下载渠道"限制) | npm 无包 |
| 阿里巴巴普惠体 | 免费商用全场景 | ✅ | npm 无包,9 字重体积巨大 |
| 得意黑 Smiley Sans | OFL-1.1 | ✅ | 仅 GitHub release 整包,直链在当前网络不可达 |
| 更纱黑体 Sarasa Gothic / Sarasa Mono | OFL-1.1 | ✅ | npm 包为空壳(需 cli 自生成切片) |
| 花园明朝 HanaMinA/B | IPA Font License | ✅(允许嵌入) | 无 npm 包,仅兜底用 |
| 站酷高端黑 / 站酷快乐体(已落) | OFL-1.1 | ✅ | 高端黑无 @fontsource 版本 |
| 阿里妈妈东方大楷 / 演示悠然小楷 | 免费商用 / 个人免费 | ⚠️ 需逐款核嵌入条款 | 无包;"个人免费"字体常禁 @font-face |
| 霞鹜文楷 Light/Bright | OFL-1.1 | ✅ | 实测 **+8605KB**(CJK 切片粒度所致) |
> 加新字体的流程:①核许可证与 **web 嵌入条款**(很多"个人免费"字体明文禁 @font-face,只能进系统回退)②npm 装或放入 `public/fonts/<family>/`(优选 cn-font-split 类切片包)③`global.css` 加类 + `rehype-class-whitelist` 放行 + `public/admin/typography-classes.js` 加一项 ④跑体积账,超 150KB/页 的要评估是否值得;展示类字体再跑一次 `scripts/build-perf/subset-fonts.py` 按实际用字裁切(命令见上节)。
```markdown
<span class="ink-muted">次要说明</span>
<span class="font-kai">金句可以换文楷</span>
<p class="drop-cap">开篇段落,首字会下沉。</p>
<p class="quote-line">所谓暮色,就是光慢慢地承认自己要走。</p>
```
- **颜色只能走上面这些类**(内部用 tokens,亮暗双态都可读);写 `style="color:red"` 会被构建期剔除
- 斜体就用普通 markdown `*斜体*`——行内强调,不会换行也不会居中
### 后台可视化选择(推荐,不用写 HTML)
后台编辑器里:**选中文字 → Insert 菜单 → 「排版(字体/颜色)」→ 面板里点一个类**,文字即被 `<span class="…">` 包住;点已套用的片段可改类,Backspace 可整体删除。
面板里的两类与上表一一对应(字体 8 / 颜色 9),类清单单一事实源在 `public/admin/typography-classes.js`。
## 三层排版架构(全站,不只文章正文)
**层 1 · 页面文案富文本**:后台「页面文案」里的 tagline / subtitle / blurb / blurb_after / intro / 关于页 blocks 的 prose 块,全部走同一条富文本管线(markdown + raw + 内容净化 + class 白名单 + 链接卡片)。可以直接写:
```yaml
tagline:
zh: '<span class="font-kai c-blue">写作者</span> · 读者 · 快门按压者,拥抱开源。'
```
渲染入口唯一:`src/lib/site-content.ts` 的 `renderRich()`(行内场景传 `{ inline: true }` 自动剥外层 `<p>`,否则 `<p>` 套 `<p>` 会被浏览器拆散)。
**不开放**:导航/页脚/按钮等硬编码界面外壳——那是 UI 不是内容。
**层 2 · 页面级排版覆盖**:每个页面单例(`content/site/*.yaml`)可加:
```yaml
typography:
font: font-kai # 本页正文默认字体类(可选)
accent: c-blue # 本页强调色类(可选)
scale: 1.1 # 字号缩放 0.8–1.25(可选)
```
实现是在该页内容容器上挂 `.page-typo` + CSS 变量(`--page-font / --page-accent / --page-scale`),**不写全站 tokens**——其它页面零影响(已在图库/书架/文章/装备页验证隔离)。白名单外的类名会被忽略。
后台入口:「页面文案 → 各自页面 → 页面排版覆盖」。
**层 3 · 艺术字体**:`font-art-kuaile`(站酷快乐体,艺术字专用类)。⚠️ **仅用于标题/短句**:艺术字字形大,CJK 切片体积高(实测用到页 +163KB),长段使用会显著抬高单页字体成本。
## 瞬间(/moments)怎么写
瞬间与文章一样支持富文本,但入口在后台「瞬间」集合(`content/moments/*.md`):
```yaml
---
pubDate: 2026-11-02
tags: [随手拍]
text: 今天把**置顶**做完了,顺手试了 <span class="c-teal">彩色</span>
---
```
- `text` 支持 markdown(`**加粗**`、列表、`> 引用`)与**同一套排版类**(字体/颜色),因为走的是与全站文案相同的净化管线:危险标签整棵丢弃,只放行 42 项白名单类
- 一条瞬间 = 一行时间线卡片,**每条都带独立评论区**(展开才挂载,双语页共用同一评论流,评论不分裂)
- 每条的「浏览 / 评论」数字是只读统计(不会因为别人点开评论而虚增浏览)
- `text` 与 `image` 至少填一项;图片建议先压缩
## 页面块(关于页 blocks,字段速查)
关于页正文可由九个块拼成(后台拖拽排序,每块有「显示」开关)。完整操作见 [管理台](./管理台.md);这里是写内容时对照的字段表——文本字段一律 `{zh, en}`。
| 块型 | 必填 | 可选 | 一句话 |
|---|---|---|---|
| `prose` | `body` | `title`、`width`(narrow/full) | markdown 长文 |
| `tracks` | `title`、`items[].name` | `items[].icon`、`items[].desc` | 主线/能力面 |
| `cards` | `items[].title` | `title`、`columns`(2/3)、`items[].desc/href/tag` | 并列卡片墙 |
| `timeline` | `items[].time`、`items[].title` | `title`、`items[].desc` | 时间线(时间可自由文本) |
| `quote` | `text` | `cite` | 单句引文 |
| `figure` | `image` | `alt`、`caption` | 单图(必须上传,禁外链) |
| `linklist` | `items[].label`、`items[].href` | `title`、`items[].note` | 链接清单 |
| `cta` | `text`、`href`、`label` | — | 行动按钮 |
| `divider` | — | `style`(line/space) | 分隔 |
```yaml
blocks:
- type: prose
body: { zh: 正文 markdown,可含排版类 span, en: Body markdown }
- type: cards
columns: 3
items:
- { title: { zh: 写作, en: Writing }, desc: { zh: 书评与技术文, en: Reviews & tech } }
```
边界:块数 ≤24、单块文本 ≤4000 字(超限截断并 WARN);未知块型跳过 + WARN;`blocks` 为空时回落 `intro` markdown,再空回落内置文案。图片字段走 `image()`,只能后台上传进 `content/site/assets/`。
## 发布提醒 ## 发布提醒
- push main → webhook 自动构建发布(约 10 秒) - push main → webhook 自动构建发布(约 10 秒)
- 新文章要英文版:跑 `python3 scripts/translate/translate_posts.py`(幂等,代码块不会被翻译破坏的风险见其 README 注释;锁定润色用 frontmatter `locked: true`) - 新文章要英文版:跑 `python3 scripts/translate/translate_posts.py`(幂等,代码块不会被翻译破坏的风险见其 README 注释;锁定润色用 frontmatter `locked: true`)
## 书封图(/books 书架)
阅读轨文章可选加书封:
1. 图片放 `content/posts/assets/books/<slug>.jpg`(建议宽 480px、高宽比约 2:3)
2. 对应文章 frontmatter 加一行:`cover: ./assets/books/<slug>.jpg`
3. 构建自动优化出 96/192 两档;**没有 cover 的文章优雅回退现有色脊卡设计**,无需改任何代码
示例 frontmatter:
```yaml
---
title: "面纱"
pubDate: 2024-06-01
category: reading
bookAuthor: "毛姆"
mood: "#8a6f4a"
cover: ./assets/books/mian-sha.jpg
---
```
## 后台上传(Sveltia 后台,推荐路径)
后台地址 `/admin/`(gitea 账号登录)。三类媒体都有后台入口,不必手写路径:
### 图片
文章编辑器里插入图片 → 自动上传到 `content/posts/assets/` 并写入 `./assets/...`
相对路径(走 Astro 图片管线,自动优化)。**不要手改成 `/assets/...` 绝对路径**。
### 视频(frontmatter 字段,正文后自动渲染)
1. 文章编辑页「附视频」字段上传(自动落 `public/media/`,路径写 `/media/<name>.mp4`)
2. 正文读完后自动出现 `<video controls preload="metadata" playsinline>`,全宽圆角黑底
3. 「视频封面」填 `/media/xxx.jpg` 可自定首帧;留空浏览器自取
4. 无视频字段 → 该区块不渲染(零占位)
体积纪律:mp4/webm ≤50MB 走 `public/media/`(构建原样拷贝,不转码);
更大的请按上文「体积纪律」放仓外 store(该场景 build.sh 同步段待首次出现时补)。
### 后台列表日期
文章/瞬间列表显示「日期 · 标题」,可按日期排序;图库照片没有 frontmatter 日期
(时间在 EXIF/文件侧),列表按「分组 · 标题」展示、按标题/分组排序。
### 链接
markdown 原生语法,无需任何配置:
```markdown
[文字](https://example.com)
```
想让链接显示成 OG 预览卡(带标题/描述/图标),把它**单独放一行**即可——规则见上文「链接卡片(OG 预览卡)」。
### 页面文案(主页/关于/开源)
后台「页面文案」下三个入口:
| 入口 | 文件 | 可改内容 |
|---|---|---|
| 主页设置 | `content/site/home.yaml` | 身份语 tagline、**头像 avatar**、**站点图标 favicon**、**社交链接 socials** |
| 关于页 | `content/site/about.yaml` | **markdown 正文 intro**(多段/列表/粗斜体/链接) |
| 开源页 | `content/site/opensource.yaml` | 叙述 blurb/blurb_after、仓链接、置顶项目 pinned |
#### 头像与站点图标
- **头像**:主页设置里「头像」上传 → 落 `content/site/assets/`,构建期优化成 60px WebP;
留空则显示内置渐变占位(w4 徽标)
- **站点图标 favicon**:先经「图标」媒体库上传(`public/icons/`),再把路径粘到
「站点图标」字段(如 `/icons/my.ico`、`/icons/my.svg`);留空回退 `/favicon.svg`。
支持 `.svg` / `.ico` / `.png`(直出,不经图片优化)
#### 社交链接
主页设置里「社交链接」是列表:可增删、拖动排序。每条 = 名称(无障碍文字)+ 链接 + 图标。
图标可选 `github / rss / email / bilibili / x`,**留空则显示纯文字按钮**。
首页名片区、以及任何引用该组件的页面都吃同一份数据(单一事实源,改一次全站生效)。
#### 首页可配项(home.yaml)
| 字段 | 取值 | 默认 | 作用 |
|---|---|---|---|
| `subtitle` | `{zh, en}`(markdown 短文本) | 空 | 身份语下方的副标题;留空不渲染 |
| `latestPhotos` | 0-6 | 4 | 「最新影像」张数;**0 = 隐藏该区块** |
| `perTrackLimit` | 4-8 | 6 | 首页阅读/技术两轨各显示条数 |
| `sections` | `{tracks, photos, heatmap, projects}` 布尔 | 全开 | 区块开关,关掉即不渲染 |
| `projectsLimit` | 1-99 | 6 | 首页项目墙条数(越界夹取) |
| `announcement` | `{zh, en}`(markdown 富文本) | 空 | 名片区下方公告;空 = 不渲染 |
| `postsTracks` | `{reading, tech, photo}` 布尔 | 全开 | 关掉某轨则该轨文章不在首页列出(归档页仍可见) |
越界值会被夹取(如 perTrackLimit=20 按 8 处理),不报错;**全部留空 = 现有形态不变**。
#### 开源页项目墙(opensource.yaml)
| 字段 | 取值 | 默认 | 作用 |
|---|---|---|---|
| `mode` | `all` / `only` / `exclude` | all | 可见性模式 |
| `include` | 仓短名数组 | 空 | mode=only 时的白名单 |
| `exclude` | 仓短名数组 | 空 | mode=exclude 时的黑名单 |
| `sort` | `pinned-first` / `stars` / `updated` | pinned-first | 排序方式 |
| `perSource` | `both` / `github` / `gitea` | both | 来源过滤 |
| `intro_from` | `"off"` / `github-readme` | off | 开篇文案来源 |
> `intro_from` 建议**带引号写**:YAML 1.1 里裸 `off` 会被解析成布尔 false。
> 选 `github-readme` 时构建期取 `wait4xx/wait4xx` 的 README 渲染为开篇,
> 成功后缓存到 `content/.github-readme.md`(入仓);token 缺位/取失败/仓无 README
> 都会打印日志并回落 yaml 的 blurb。注意 README 里的相对链接与外部徽章会原样输出。
#### 首页文章列表
首页现在分两段:**阅读轨 6 条 + 技术轨 6 条**(各自带「全部文章」出口:阅读→书架、技术→归档)。
照片轨文章不进首页(在图库看)。完整列表见 `/archives/`。
- 中文/英文各一个字段,页面按语言取;**留空则该字段回落默认文案**(构建不会炸)
- `pinned` 填仓库短名(如 `blog`、`pi-board`),**顺序即展示顺序**;留空回落 `blog`
+184
View File
@@ -1,17 +1,201 @@
# 管理台(Sveltia CMS)使用指南 # 管理台(Sveltia CMS)使用指南
> 地址:`https://wait4x.online/admin/`(页脚「管理」入口)| 用途:照片上传与 sidecar 编辑的图形界面 | 保存即 commit 到 main → webhook 自动构建发布(约 10 秒上线) > 地址:`https://wait4x.online/admin/`(页脚「管理」入口)| 用途:照片上传与 sidecar 编辑的图形界面 | 保存即 commit 到 main → webhook 自动构建发布(约 10 秒上线)
>
> 这份是**操作手册**:每个入口能改什么、字段怎么填、改完会发生什么。写文章正文的排版技巧见 [写作指南](./写作指南.md)——两份分工:这里讲"后台怎么用",那里讲"正文怎么写"。
## 登录 ## 登录
选 **Sign In with Token** → 用 gitea 的 access token(宿主 `~/docker_apps/gitea/init-templates.txt` 里的 api-token,或 gitea 网页新建 scope=read/write:repository)。token 存浏览器 localStorage,下次免登。 选 **Sign In with Token** → 用 gitea 的 access token(宿主 `~/docker_apps/gitea/init-templates.txt` 里的 api-token,或 gitea 网页新建 scope=read/write:repository)。token 存浏览器 localStorage,下次免登。
## 四类内容集合
| 入口 | 文件位置 | 用来放什么 | 关键约束 |
|---|---|---|---|
| 文章 | `content/posts/<slug>.md` | 书评 / 技术文 / 摄影札记三轨 | `category` 三选一(`reading` / `tech` / `photo`),首页分轨由它派生 |
| 瞬间 | `content/moments/*.md` | 一句话 + 一张图的时间线 | `text` 与 `image` **至少填一项**(后台无法表达条件必填,保存前自查) |
| 照片 | `content/photos/*.json` + 图 | 图库(街拍 / 风景两组) | 标题**必填**(空标题会让构建炸,实测坑);EXIF 由构建期 loader 自动补全 |
| 站点页面 | `content/site/*.yaml` | 首页、关于页、开源页、主题 | 后台改的是**单例文件**,没有"新建" |
几条通用规则:
- **双语字段成对出现**:中文 / 英文两个输入框。英文留空不是错误——构建期回落中文并在日志里打 WARN(`[blocks] 缺译…`),页面不会破版。
- **留空 = 用默认**:任何字段清空都回到仓库自带的默认文案 / 默认值,构建永远成功。想让某段彻底消失,请看字段自带的"留空则不渲染"提示。
- **文章英文版**:不在后台写。跑 `python3 scripts/translate/translate_posts.py` 生成 `content/posts-en/` 同名文件;手写润色过的译文在 frontmatter 写 `locked: true`,翻译脚本会无条件跳过它。
- **保存即提交**:后台保存等于向仓库提交,push 后约 10 秒自动构建上线。
## 文章置顶(5h 新增)
文章编辑页有一个「置顶」开关。打开后这篇文章排在首页分轨、归档、书架三处列表的最前面,列表里显示一个灰阶「置顶」小徽标。
语义边界(刻意如此,别当 bug 报):
- **只影响列表顺序**:`pubDate` 不变、URL 不变、RSS 里的时间序列不变,订阅者不会看到时间错乱
- **支持多篇/多本同时置顶**:置顶组内部仍按日期倒序;书架页(/books)里每个置顶的书评都会排在前面并**默认展开**
- 书架页用「↑ + 横线」的回到顶部图标表示置顶(不写文字),与右侧展开箭头形状不同,不会混读
- 关掉开关立即恢复时间序,没有"冷却期"
## 瞬间(5h 起与文章同级)
「瞬间」集合的「文字」字段是 **markdown 编辑器**,与文章同一套表达力:
- markdown:`**加粗**`、`*斜体*`、列表、`> 引用`、`` `行内代码` ``
- 排版类 HTML:`<span class="c-blue font-kai">彩色 + 楷体</span>`(可用类清单见写作指南「文章排版类」一节,与文章完全同一份白名单)
- 彩色**不再限次**(原来的"一页最多三处"已取消);仍守外壳灰阶:导航/页脚/按钮/背景不染色
**每条瞬间自带评论区**:时间线卡片下方有「评论」折叠条,读者点开才加载评论(不点开不为它付任何加载成本)。中文页与英文页共用同一条评论流,语言切换不会丢评论。
**每条的浏览 / 评论数字是只读的**:读者点开评论区不会虚增浏览数(只有整页访问才计一次)。
## 照片说明文字(灯箱右侧)支持排版(5i 新增)
照片的「说明」(caption)现在支持 markdown 与排版类,与文章正文同一套:
```yaml
caption: 落日把海与城一齐烫成金箔,**这一帧等了二十分钟**
```
```html
<span class="c-blue">蓝色</span>、<span class="font-kai">楷体</span> 这些类都能用。
```
灯箱是深底,颜色类会自动取柔和对暗底的那一档,不会刺眼。EXIF 那几行是相机数据,不做排版。
## 照片上传流 ## 照片上传流
1. 图库照片 → New Entry:填标题(**必填**,空标题会让构建炸——实测坑)、选分组(street/landscape)、可写 alt 1. 图库照片 → New Entry:填标题(**必填**,空标题会让构建炸——实测坑)、选分组(street/landscape)、可写 alt
2. 图片字段 → 资产库上传 jpg(与 sidecar 同目录落位,零重压缩) 2. 图片字段 → 资产库上传 jpg(与 sidecar 同目录落位,零重压缩)
3. Save → 自动 commit + 构建 → 画廊出现(EXIF 构建期自动提取,exif 字段留空即可) 3. Save → 自动 commit + 构建 → 画廊出现(EXIF 构建期自动提取,exif 字段留空即可)
## 块编辑:关于页的块式页面构建器
关于页(`content/site/about.yaml`)的正文由 **blocks 列表**构成,后台里是一串可拖拽排序的卡片。每个块都有「显示」开关;关掉只是不渲染,块本身留在数据里。
两条渲染纪律:
- 列表为空(或全关)时,页面回落到 `intro` 字段的 markdown 正文;`intro` 也空则用内置默认文案。三级回落,任何一级为空都不会出白页。
- 未知块型(手工编辑 yaml 写错的 `type`)会被**跳过并打印 `[blocks]` 警告**,不会炸构建;块数上限 24,单块文本上限 4000 字,超限截断并 WARN。
九类块与字段(文本字段一律 `{zh, en}` 双语对象):
| 块型 | 必填 | 可选 | 用途 |
|---|---|---|---|
| `prose` 正文 | `body` | `title`、`width`(`narrow` 版心 / `full` 通栏) | markdown 长文,最常用 |
| `tracks` 版图 | `title`、`items[].name` | `items[].icon`、`items[].desc` | 几条主线 / 能力面,图标白名单外或留空显示首字占位 |
| `cards` 卡片墙 | `items[].title` | `title`、`columns`(2 或 3,默认 3)、`items[].desc/href/tag` | 并列展示若干条目 |
| `timeline` 时间线 | `items[].time`、`items[].title` | `title`、`items[].desc` | 时间列是自由文本(`2024 — 2026` 也行) |
| `quote` 引文 | `text` | `cite` | 单句金句,配 `font-kai` 更出效果 |
| `figure` 图片 | `image` | `alt`、`caption` | **只能后台上传**(落 `content/site/assets/`,走图片管线),禁止外链 |
| `linklist` 链接清单 | `items[].label`、`items[].href` | `title`、`items[].note` | 相关阅读 / 外部资源 |
| `cta` 行动按钮 | `text`、`href`、`label` | — | 一页建议不超过一个 |
| `divider` 分隔 | — | `style`(`line` 细线 / `space` 纯留白) | 长页分段 |
yaml 形态示例(手工编辑时照此写):
```yaml
blocks:
- type: prose
title: { zh: 关于这个站点, en: About this site }
body:
zh: 这里写 markdown 正文,支持列表、粗斜体、链接与排版类。
en: Markdown body here.
width: narrow
- type: quote
text: { zh: 所谓暮色,就是光慢慢地承认自己要走。, en: Dusk is light admitting it has to go. }
cite: { zh: 本站, en: this site }
- type: figure
image: ./assets/desk.jpg # 后台会替你写成这个相对路径
caption: { zh: 工作台一角, en: A corner of the desk }
```
其它可用字段:`layout.showIdCard`(关掉则不渲染名片区,默认开)、`typography`(见下一节)。
**预览限制**:后台只提供字段编辑与列表摘要(摘要显示块型名),块的真实排版以下次构建后的页面为准——想调间距 / 字号,请用「主题设置」而不是在块里硬凑空行。
## 页面排版覆盖(每页一处)
首页 / 关于页 / 开源页各自有一个「页面排版覆盖(可选)」对象:
| 字段 | 取值 | 说明 |
|---|---|---|
| `font` | 字族下拉(10 项) | 本页**正文**默认字体类 |
| `accent` | 颜色下拉(18 项) | 本页强调色类 |
| `scale` | 0.8–1.25 | 字号缩放,1 = 全站默认 |
- 只作用于**该页内容容器**(挂 `.page-typo` + CSS 变量),不写全站 tokens,其它页面零影响。
- **体积成本提醒**:整页换艺术/展示字族(`font-huiwen` 汇文、`font-kuaile` 快乐体)意味着整页正文都要下载对应切片——单页字体成本会明显抬升,见写作指南的体积账。
- 白名单外的类名会被忽略(构建期过滤),写错不报错、也不生效。
## 主题设置(全站 17 个旋钮)
「主题设置」入口写 `content/site/theme.yaml`,17 项全部可选,留空 = 仓库默认值。**配错不会炸站**:越界值回落默认,组内单调性被破坏则整组回落。
| 组 | 字段 | 范围 | 约束语义 |
|---|---|---|---|
| 强调色 | `accent_light` / `accent_dark` | `#rrggbb` | 明暗各一份;构建期对比度闸门断言正文 ≥4.5:1,不达标构建失败 |
| 圆角 | `radius_img` | 4–16 | 图片圆角,建议 ≤14 |
| 标题字号 | `text_h1` / `text_h2` / `text_h3` | h1/h2:22–32,h3:16–32 | **必须严格递减**(h1 > h2 > h3),否则三档一起回落默认 |
| 正文字号 | `prose_size_reading` / `prose_size_tech` | 16–22 | 书评 / 技术文分轨独立 |
| 行高 | `leading_reading` / `leading_tech` | 1.5–2.0 | 同上分轨 |
| 间距梯度 | `space_3` / `space_4` / `space_5` / `space_6` | 8–80 | **必须严格递增**,否则整组回落 |
| 流距 | `flow_p` / `flow_h2_post` | 0–2(em) | 段间距 / 标题下间距 |
| 动效 | `dur` | 150–600(ms) | 过渡时长;`prefers-reduced-motion` 下自动归零 |
不能配的东西(有意为止):版心宽度、断点、纸墨色、代码配色——它们牵动版面结构与分轨身份,属于设计红线。注入方式是构建期内联 `:root:root{…}`,运行时零 JS、零请求。
## 首页与开源页的其余旋钮
### 首页三字段(5g 新增)
| 字段 | 取值 | 默认 | 作用 |
|---|---|---|---|
| `projectsLimit` | 1–99 | 6 | 首页项目墙条数(越界夹取) |
| `announcement` | `{zh, en}` markdown | 空 | 名片区下方公告;**空 = 不渲染**,走富文本管线(可写排版类 span) |
| `postsTracks` | `{reading, tech, photo}` 布尔 | 全开 | 关掉某轨,该轨文章不在首页列出(归档页仍可见) |
### 开源页(opensource.yaml)
**开篇是原生「GitHub 档案卡」(5h 起)**:头像 / 名字@handle / 定位叙述 / 社交图标 / 统计四格(关注者、公开仓、总星标)/ 语言占比灰阶条(只有第一名用强调色,其余灰阶)。**零第三方图片、零 iframe**。数据失败只隐藏对应数字,不会破版。
想换那段「定位叙述」,改本文件的 `blurb` + `blurb_after`(双语,后台可改)——它们现在同时供给档案卡与项目墙下方的说明。
下面的 `intro_from: "github-readme"` **仍可用但不推荐**:那会把 GitHub 专用 README 原样渲染,带 7 类外链图(shields.io、readme-typing-svg、github-readme-stats、streak-stats、activity-graph、snake、komarev 访客徽章)与 `<div align="center">` 居中——本站按普通块渲染,故居中失效;外链图在慢网或被墙时全空白。这也是当初"部分内容没居中、有些图片没显示"的根因。
| 字段 | 取值 | 说明 |
|---|---|---|
| `pinned` | 仓短名数组 | 置顶顺序即展示顺序 |
| `mode` | `all` / `only` / `exclude` | 可见性模式 |
| `include` / `exclude` | 仓短名数组 | 对应模式的白 / 黑名单 |
| `sort` | `pinned-first` / `stars` / `updated` | 排序 |
| `perSource` | `both` / `github` / `gitea` | 来源过滤 |
| `intro_from` | `"off"` / `"github-readme"` | 开篇文案来源(**带引号写**,YAML 1.1 里裸 `off` 是布尔 false) |
**`mode: only` 的"看不见"排查**:构建日志会给每个没显示出来的 include 项打一行 `[opensource] projects:include 里的「X」…`,并区分两种原因——双源结果里根本没有这个仓(拼写 / 私有仓 / 该源刚失败),或者命中了但被 `perSource` 过滤掉。看到哪一行就知道该改什么。
**README 开篇(`intro_from: "github-readme"`)的回落矩阵**,构建日志逐级点名:
1. 取到 → 内容有变化才刷新缓存 `content/.github-readme.md`(入仓,保证构建确定性)
2. token 缺位 / HTTP 非 2xx / 内容为空 / 超时(10 秒)/ 网络异常 → 改用缓存(日志标注"可能陈旧")
3. 缓存也不在 → 回落本文件 `blurb` 的叙述文案
4. 缓存回写失败(构建仓只读挂载等)→ 只告警,本次渲染照常
token 位在 `~/docker_apps/blog/github-token.txt`(本机,0600)。
**两条已知限制**:
- **README 里的相对链接不重写**(如 `./img/x.png`、`docs/foo.md`):它们相对 GitHub 仓根解析,在本站没有对应产物,点了多半 404。保原样比猜一个基址可信,所以不做改写。
- **外部徽章图片加固但不参与构建**:shields.io、readme-typing-svg 这类第三方徽章在构建期**不抓取**,渲染时统一补 `loading="lazy"`(缺 alt 补空),它们慢、被墙或 404 都不影响页面渲染;尺寸已由样式限制在内容宽度内。
## 浏览与评论计数(5h 新增)
文章页、瞬间页、开源页、关于页都用同一枚灰阶徽标:**眼睛图标 = 浏览数,气泡图标 = 条评论**,数据来自自建 Artalk,不依赖任何第三方徽章服务。
几条行为约定(省得当成 bug):
- **一页只计一次浏览**:「计一次」发生在这页唯一那个 `page` 模式徽标(文章页在标题下方),同一页其余徽标都是只读镜像,显示的是同一个数字——不会出现"页头 17、评论区 16"
- **开评论不算浏览**:瞬间逐条徽标是只读的,读者展开评论区不会给那条动态虚增浏览量
- **接口挂掉显示「–」而不是 0**:宁可留白,也不给读者看一个错误的零
- **换页重计、同页刷新重计**:这是浏览量的定义,不是重复计数的 bug(软导航回同一页也算新一次访问)
## 纪律(与写作指南同源) ## 纪律(与写作指南同源)
- **图片体积**:上传前自行压缩(长边 ≤2400px)——Sveltia 内建优化**刻意关闭**(会抹 EXIF 断构建期提取管线) - **图片体积**:上传前自行压缩(长边 ≤2400px)——Sveltia 内建优化**刻意关闭**(会抹 EXIF 断构建期提取管线)
+28 -1
View File
@@ -6,15 +6,21 @@
# 发布。改为独立构建仓 fetch+reset 硬跟 main:线上=远端 main,确定性闭环。 # 发布。改为独立构建仓 fetch+reset 硬跟 main:线上=远端 main,确定性闭环。
# blog 仓已公开,匿名 fetch 无需凭据。 # blog 仓已公开,匿名 fetch 无需凭据。
set -e set -e
# 抓取 URL:默认公网;同机构建可由宿主环境变量覆盖为内网
# (2026-10-26 事故:ESA 边缘对 git.wait4x.online 522 时公网 fetch 失败,
# set -e 下静默死、staging 冻结——内网通道必须可用)
BLOG_GIT_URL="${BLOG_GIT_URL:-https://git.wait4x.online/wait4x/blog.git}"
BUILD_REPO="$HOME/docker_apps/blog/build-repo" BUILD_REPO="$HOME/docker_apps/blog/build-repo"
STAGING="$HOME/docker_apps/blog/www-staging" STAGING="$HOME/docker_apps/blog/www-staging"
if [ ! -d "$BUILD_REPO/.git" ]; then if [ ! -d "$BUILD_REPO/.git" ]; then
echo "[build] 首次克隆构建仓" echo "[build] 首次克隆构建仓"
git clone https://git.wait4x.online/wait4x/blog.git "$BUILD_REPO" git clone "$BLOG_GIT_URL" "$BUILD_REPO"
fi fi
cd "$BUILD_REPO" cd "$BUILD_REPO"
# 每次构建把 origin 指向配置的 URL(同机构建走内网通道;公网 522 时仍可发布)
git remote set-url origin "$BLOG_GIT_URL"
git fetch origin main git fetch origin main
git reset --hard origin/main git reset --hard origin/main
git clean -fd --exclude node_modules git clean -fd --exclude node_modules
@@ -27,6 +33,21 @@ pnpm astro build
# 按页字体 CSS 子集化(车道 A 任务 1):裁 @font-face 声明数,构建仓与本地同链; # 按页字体 CSS 子集化(车道 A 任务 1):裁 @font-face 声明数,构建仓与本地同链;
# 存在性守卫:新文件未推送到 main 前构建仓无此脚本,绝不让发布链因车道在途文件断掉 # 存在性守卫:新文件未推送到 main 前构建仓无此脚本,绝不让发布链因车道在途文件断掉
[ -f scripts/build-perf/subset-font-css.mjs ] && node scripts/build-perf/subset-font-css.mjs dist || echo "[build] subset 脚本未就绪,跳过" [ -f scripts/build-perf/subset-font-css.mjs ] && node scripts/build-perf/subset-font-css.mjs dist || echo "[build] subset 脚本未就绪,跳过"
[ -f scripts/build-perf/config-check.py ] && python3 scripts/build-perf/config-check.py || echo "[build] config-check 未就绪,跳过"
[ -f scripts/build-perf/contrast-check.mjs ] && node scripts/build-perf/contrast-check.mjs dist || echo "[build] contrast 脚本未就绪,跳过"
# 首页阻塞 CSS 内联(车道 D):守卫先行同上——脚本未到 main 前绝不炸链
[ -f scripts/build-perf/gen-sw-precache.mjs ] && node scripts/build-perf/gen-sw-precache.mjs dist || echo "[build] sw-precache 生成器未就绪,跳过"
[ -f scripts/build-perf/inline-home-css.mjs ] && node scripts/build-perf/inline-home-css.mjs || echo "[build] inline-home 脚本未就绪,跳过"
# README 动效资产(5i T8):**不在此跑抓取**(避免发布链被外网抖动拖累),只做存在性守卫 + 缺资产告警
if [ -f public/github-assets/manifest.json ]; then
echo "[build] github-assets 就绪:$(find public/github-assets -name '*.svg' | wc -l) 个自托管 SVG"
else
echo "[build] WARN public/github-assets/manifest.json 缺失——开源页数据板将只显示原生统计(可手动跑 node scripts/build-perf/fetch-readme-assets.mjs 补齐)"
fi
# 内容 lint(T3):断链/缺图在部署前拦截;CONTENT_LINT=skip|warn 逃生门见脚本头注
[ -f scripts/build-perf/content-lint.mjs ] && node scripts/build-perf/content-lint.mjs dist || echo "[build] content-lint 未就绪,跳过"
echo "[build] rsync -> $STAGING" echo "[build] rsync -> $STAGING"
mkdir -p "$STAGING" mkdir -p "$STAGING"
@@ -35,3 +56,9 @@ ORIG="$HOME/docker_apps/blog/photos-originals"
if [ -d "$ORIG" ]; then rsync -a "$ORIG/" "$STAGING/photos-originals/"; fi if [ -d "$ORIG" ]; then rsync -a "$ORIG/" "$STAGING/photos-originals/"; fi
printf 'built %s at %s\n' "$SHA" "$(date -Is)" > "$STAGING/.build-stamp" printf 'built %s at %s\n' "$SHA" "$(date -Is)" > "$STAGING/.build-stamp"
echo "[build] done: $(find "$STAGING" -name '*.html' | wc -l) html files @ $SHA" echo "[build] done: $(find "$STAGING" -name '*.html' | wc -l) html files @ $SHA"
# 生产冒烟(T3):staging 三断言。首版容忍失败(robots 待 T1 落地,smoke FAIL 属预期),
# T1 收编后改硬门:去掉 `|| echo` 让断言失败直接炸构建
if [ -f scripts/build-perf/prod-smoke.sh ] && [ "${SMOKE_BASE:-http://127.0.0.1:18743}" != "off" ]; then
bash scripts/build-perf/prod-smoke.sh "${SMOKE_BASE:-http://127.0.0.1:18743}" || echo "[build] prod-smoke 未过(见上;T1 robots 落地后转硬门)"
fi
+9 -5
View File
@@ -85,11 +85,15 @@ class Handler(BaseHTTPRequestHandler):
def _run_build(self) -> None: def _run_build(self) -> None:
with BUILD_LOCK: with BUILD_LOCK:
subprocess.run( # 输出落盘:capture_output 丢弃是调试黑洞(2026-10-26 ESA 522
["bash", str(build_script())], # 事件中构建静默死、staging 冻结无任何可见痕迹)
capture_output=True, timeout=1200, log = Path.home() / "docker_apps/blog/build.log"
cwd=str(BUILD_REPO), with open(log, "wb") as fh:
) subprocess.run(
["bash", str(build_script())],
stdout=fh, stderr=subprocess.STDOUT, timeout=1200,
cwd=str(BUILD_REPO),
)
def log_message(self, fmt: str, *args: object) -> None: def log_message(self, fmt: str, *args: object) -> None:
# 静默常规访问日志(健康探针会刷屏);错误仍走默认 stderr # 静默常规访问日志(健康探针会刷屏);错误仍走默认 stderr
+5 -1
View File
@@ -16,13 +16,17 @@
"@fontsource-variable/noto-serif-sc": "^5.3.1", "@fontsource-variable/noto-serif-sc": "^5.3.1",
"@fontsource/jetbrains-mono": "^5.3.0", "@fontsource/jetbrains-mono": "^5.3.0",
"@fontsource/noto-sans-sc": "^5.3.1", "@fontsource/noto-sans-sc": "^5.3.1",
"astro": "^7.3.8", "@fontsource/zcool-kuaile": "^5.3.0",
"astro": "^7.4.4",
"astro-pagefind": "^2.0.1", "astro-pagefind": "^2.0.1",
"exifr": "^7.1.3", "exifr": "^7.1.3",
"lxgw-wenkai-screen-webfont": "^1.7.0", "lxgw-wenkai-screen-webfont": "^1.7.0",
"pdfjs-dist": "^6.4.299",
"rehype-raw": "^7.0.0",
"sharp": "^0.35.5" "sharp": "^0.35.5"
}, },
"devDependencies": { "devDependencies": {
"@astrojs/markdown-remark": "^7.3.5",
"@fontsource/noto-serif-sc": "^5.3.1", "@fontsource/noto-serif-sc": "^5.3.1",
"playwright": "^1.64.1" "playwright": "^1.64.1"
} }
+1465 -71
View File
File diff suppressed because it is too large. Load diff
+584 -2
View File
@@ -5,8 +5,10 @@ backend:
name: gitea name: gitea
repo: wait4x/blog repo: wait4x/blog
branch: main branch: main
base_url: https://git.wait4x.online # 走博客域同源反代(nginx /gitea/ → 127.0.0.1:13020):
api_root: https://git.wait4x.online/api/v1 # ①绕开 git.wait4x.online 的 ESA 522 ②同源无 CORS ③ESA 缓存规则已排除 /artalk/api
base_url: https://wait4x.online/gitea
api_root: https://wait4x.online/gitea/api/v1
media_folder: content/photos media_folder: content/photos
site_url: https://wait4x.online site_url: https://wait4x.online
@@ -16,6 +18,96 @@ output:
omit_empty_optional_fields: true omit_empty_optional_fields: true
collections: collections:
# 文章(含书评/技术/照片三轨;书=category:reading 的文章,同一集合管理)
- name: posts
label: 文章
label_singular: 文章
folder: content/posts
create: true
# 中文主稿;英文为 TMT 预翻译入仓的 .en.md(posts-en 集合不在后台,防误改译文)
slug: '{{slug}}'
summary: '{{pubDate}} · {{title}}'
sortable_fields: [pubDate, title]
# 注意:Sveltia 对 folder 集合的 media_folder 按"相对集合目录"解析
# (写成 content/posts/assets 会二次嵌套成 content/posts/content/posts/assets,
# 实测 2026-11-02 首上传即踩),故这里必须写 assets
media_folder: assets
public_folder: ./assets
fields:
- { label: 标题, name: title, widget: string }
- { label: 日期, name: pubDate, widget: datetime, date_format: 'YYYY-MM-DD', time_format: false, format: 'YYYY-MM-DD' }
- { label: 分轨, name: category, widget: select, options: [{label: 阅读, value: reading}, {label: 技术, value: tech}, {label: 照片, value: photo}] }
- { label: 标签, name: tags, widget: list, required: false }
- { label: 置顶, name: pinned, widget: boolean, required: false, default: false, hint: '置顶文章排在首页/归档/书架最前;只影响顺序,不改日期、URL 与 RSS 时间语义' }
- { label: 情绪色(书评色脊), name: mood, widget: color, required: false, hint: '#RRGGBB,仅阅读轨使用' }
- { label: 原书作者, name: bookAuthor, widget: string, required: false }
- { label: 一句话描述(书架策展语), name: description, widget: text, required: false }
- { label: 书封图, name: cover, widget: image, required: false, hint: '上传后自动生成,无需手填路径;仅阅读轨展示' }
- label: 附视频(可选)
name: video
widget: file
required: false
media_folder: public/media
public_folder: /media
hint: '展示在正文之后;mp4/webm ≤50MB(走 public/media,构建时原样拷贝,不做转码)'
- label: 视频封面(可选)
name: poster
widget: string
required: false
hint: '填 /media/xxx.jpg(先上传到 public/media 取路径);缺省浏览器自取首帧'
- { label: 隐藏(不进列表/sitemap), name: hiddenTag, widget: boolean, required: false, hint: '对应 tags 含 hidden;保存后请人工核对' }
- { label: 草稿, name: draft, widget: boolean, required: false, default: false }
- { label: 正文, name: body, widget: markdown }
# 主题旋钮(样式即数据 · 层 4):只放白名单内的 9 组,越界值会被构建期钳制
- name: site_theme
label: 主题设置
# 文件集合必须是 files: [{file,label,fields}] 结构;把 file 直接写在集合层级会被 Sveltia 判非法(2026-11-02 实测报 "The file option ... is invalid")
files:
- name: theme
label: 主题设置
file: content/site/theme.yaml
format: yaml
fields:
- { label: 强调色(亮), name: accent_light, widget: color, required: false, hint: '亮色模式强调色,如 #3454d1;构建期校验正文对比度 ≥4.5' }
- { label: 强调色(暗), name: accent_dark, widget: color, required: false, hint: '暗色模式强调色,如 #8ea2ff' }
- { label: 图片圆角, name: radius_img, widget: number, required: false, value_type: int, min: 4, max: 16, hint: '范围 4–16(建议 ≤14)' }
- { label: 一级标题字号, name: text_h1, widget: number, required: false, value_type: int, min: 22, max: 32, hint: '22–32,且必须大于 h2' }
- { label: 二级标题字号, name: text_h2, widget: number, required: false, value_type: int, min: 22, max: 32, hint: '22–32,必须小于 h1、大于 h3' }
- { label: 三级标题字号, name: text_h3, widget: number, required: false, value_type: int, min: 16, max: 32, hint: '16–32,必须小于 h2' }
- { label: 书评正文字号, name: prose_size_reading, widget: number, required: false, value_type: int, min: 16, max: 22, hint: '16–22' }
- { label: 技术文正文字号, name: prose_size_tech, widget: number, required: false, value_type: int, min: 16, max: 22, hint: '16–22' }
- { label: 书评行高, name: leading_reading, widget: number, required: false, value_type: float, min: 1.5, max: 2.0, step: 0.05, hint: '1.5–2.0' }
- { label: 技术文行高, name: leading_tech, widget: number, required: false, value_type: float, min: 1.5, max: 2.0, step: 0.05, hint: '1.5–2.0' }
- { label: 间距 3, name: space_3, widget: number, required: false, value_type: int, min: 8, max: 80, hint: '8–80,必须 < 间距 4' }
- { label: 间距 4, name: space_4, widget: number, required: false, value_type: int, min: 8, max: 80, hint: '8–80,必须 > 间距 3 且 < 间距 5' }
- { label: 间距 5, name: space_5, widget: number, required: false, value_type: int, min: 8, max: 80, hint: '8–80,必须 > 间距 4 且 < 间距 6' }
- { label: 间距 6, name: space_6, widget: number, required: false, value_type: int, min: 8, max: 80, hint: '8–80,必须 > 间距 5' }
- { label: 段间距, name: flow_p, widget: number, required: false, value_type: float, min: 0, max: 2, step: 0.05, hint: '0–2(em)' }
- { label: 标题下间距, name: flow_h2_post, widget: number, required: false, value_type: float, min: 0, max: 2, step: 0.05, hint: '0–2(em)' }
- { label: 过渡时长, name: dur, widget: number, required: false, value_type: int, min: 150, max: 600, hint: '150–600(毫秒)' }
# 瞬间:随手发(text/image 至少其一——Sveltia 无法表达条件必填,两者都设可选,
# 保存前自查;pubDate 必填)。图片走图库媒体库(content/photos 同源可复用)
- name: moments
label: 瞬间
folder: content/moments
create: true
slug: '{{year}}-{{month}}-{{day}}-{{slug}}'
summary: '{{fields.pubDate}} · {{fields.text}}'
sortable_fields: [pubDate, text]
fields:
- { label: 时间, name: pubDate, widget: datetime, date_format: 'YYYY-MM-DD', time_format: false, format: 'YYYY-MM-DD' }
- label: 文字
name: text
widget: markdown
required: false
hint: '与图片至少填一项。支持 markdown(**加粗**、列表、> 引用)与排版类 HTML,例如 <span class="c-blue font-kai">彩色+楷体</span>;装饰色仍守“外壳灰阶、彩色只作强调”的纪律'
- { label: 图片, name: image, widget: image, required: false, hint: '与文字至少填一项;建议先压缩' }
- label: 标签
name: tags
widget: list
required: false
- name: photos - name: photos
label: 图库照片 label: 图库照片
folder: content/photos folder: content/photos
@@ -26,12 +118,15 @@ collections:
media_folder: '' media_folder: ''
public_folder: '' public_folder: ''
summary: '{{fields.group}} · {{fields.title}}' summary: '{{fields.group}} · {{fields.title}}'
# 照片无 frontmatter 日期(JSON sidecar,时间在 exif/文件侧)——列表按标题/分组排序
sortable_fields: [title, group]
fields: fields:
# title 必填:Astro schema z.string() 无 .optional(),omit 纪律下必需且有值才会落键(空 title 会让 main 不可构建) # title 必填:Astro schema z.string() 无 .optional(),omit 纪律下必需且有值才会落键(空 title 会让 main 不可构建)
- { label: 标题, name: title, widget: string, required: true } - { label: 标题, name: title, widget: string, required: true }
- { label: 图片, name: image, widget: image } - { label: 图片, name: image, widget: image }
- { label: 分组, name: group, widget: select, options: [street, landscape] } - { label: 分组, name: group, widget: select, options: [street, landscape] }
- { label: 替代文本, name: alt, widget: string, required: false } - { label: 替代文本, name: alt, widget: string, required: false }
- { label: 人为描述, name: caption, widget: text, required: false, hint: 展示在灯箱里的人为描述(一句话策展文案) }
- label: EXIF 覆盖(一般留空,构建期自动从图提取) - label: EXIF 覆盖(一般留空,构建期自动从图提取)
name: exif name: exif
widget: object widget: object
@@ -42,3 +137,490 @@ collections:
- { label: 光圈值, name: fNumber, widget: number, required: false } - { label: 光圈值, name: fNumber, widget: number, required: false }
- { label: 曝光秒, name: exposureTime, widget: number, required: false } - { label: 曝光秒, name: exposureTime, widget: number, required: false }
- { label: ISO, name: iso, widget: number, required: false } - { label: ISO, name: iso, widget: number, required: false }
# 页面文案单例(cms 车道 A):三个文件型入口,保存即写 content/site/*.yaml,
# 字段与 Astro siteHome/siteAbout/siteOpenSource 的 schema 一一对应(zh/en 双字段,留空回落默认)
- name: site_pages
label: 页面文案
files:
- name: home
label: 主页设置
file: content/site/home.yaml
format: yaml
# 头像上传落 content/site/assets/,yaml 写 ./assets/xxx(Astro 图片管线优化)
media_folder: content/site/assets
public_folder: ./assets
fields:
- label: 身份语
name: tagline
widget: object
required: false
hint: 首页大字身份语(双语,markdown 富文本);可写 <span class="font-kai c-blue">…</span> 做局部强调
fields:
- { label: 中文, name: zh, widget: string, required: false }
- { label: 英文, name: en, widget: string, required: false }
- label: 页面排版覆盖(可选)
name: typography
widget: object
required: false
hint: 本页正文默认字体 / 强调色 / 字号缩放(0.8–1.25);不填用全站默认,只影响本页
fields:
- label: 正文字体类
name: font
widget: select
required: false
hint: 整页正文换字族;艺术/展示类字族体积大。5i 新增六款(小薇/庆科黄油/马善政/柳建毛草/得意黑/新晰黑)均按站内用字切片,单页增量 27–46KB;站外用字(生僻字)会回落系统字族,需按 docs 里的命令重切
options: [font-serif, font-kai, font-sans, font-mono, font-song, font-kai-sys, font-hei-sys, font-fangsong, font-huiwen, font-kuaile, font-xiaowei, font-huangyou, font-mashanzheng, font-liujianmaocao, font-smiley, font-neoxihei]
- label: 强调色类
name: accent
widget: select
required: false
options: [ink-accent, ink-accent-soft, c-blue, c-blue-soft, c-teal, c-teal-soft, c-green, c-green-soft, c-yellow, c-yellow-soft, c-orange, c-orange-soft, c-red, c-red-soft, c-pink, c-pink-soft, c-purple, c-purple-soft]
- { label: 字号缩放, name: scale, widget: number, required: false, value_type: float, min: 0.8, max: 1.25, step: 0.05, hint: '0.8–1.25;1 = 全站默认' }
- label: 公告(可选,markdown)
name: announcement
widget: object
required: false
hint: 渲染在名片区下方;留空则不显示公告块
fields:
- { label: 中文, name: zh, widget: markdown, required: false }
- { label: 英文, name: en, widget: markdown, required: false }
- { label: 项目墙条数, name: projectsLimit, widget: number, required: false, value_type: int, min: 1, max: 99, hint: '首页项目墙显示几条(1–99,缺省 6)' }
- label: 轨细分开关(首页文章列表)
name: postsTracks
widget: object
required: false
hint: 关掉某轨则该轨文章不在首页列出(全开=现状)
fields:
- { label: 阅读轨, name: reading, widget: boolean, required: false, default: true }
- { label: 技术轨, name: tech, widget: boolean, required: false, default: true }
- { label: 照片轨, name: photo, widget: boolean, required: false, default: true }
- label: 副标题(可选,markdown 短文本)
name: subtitle
widget: object
hint: 渲染在身份语下方;留空不渲染
fields:
- { label: 中文, name: zh, widget: string, required: false }
- { label: 英文, name: en, widget: string, required: false }
- label: 最新影像张数
name: latestPhotos
widget: number
required: false
default: 4
min: 0
max: 6
hint: '1-6;填 0 = 隐藏「最新影像」区块。留空默认 4'
- label: 首页每轨文章条数
name: perTrackLimit
widget: number
required: false
default: 6
min: 4
max: 8
hint: 阅读轨/技术轨各显示条数,留空默认 6
- label: 区块开关
name: sections
widget: object
required: false
hint: 关掉的区块不渲染(默认全开)
fields:
- { label: 文章列表, name: tracks, widget: boolean, required: false, default: true }
- { label: 最新影像, name: photos, widget: boolean, required: false, default: true }
- { label: 贡献热力图, name: heatmap, widget: boolean, required: false, default: true }
- { label: 项目墙, name: projects, widget: boolean, required: false, default: true }
- label: 头像(可选)
name: avatar
widget: image
required: false
hint: '留空 = 内置渐变占位;上传后自动优化为 60px WebP'
- label: 站点图标 favicon
name: favicon
widget: string
required: false
hint: 'public 下的路径,如 /icons/my.ico 或 /icons/my.svg;留空回退 /favicon.svg。经「图标」媒体库上传'
- label: 社交链接(顺序即展示顺序)
name: socials
widget: list
required: false
summary: '{{fields.label}} → {{fields.url}}'
fields:
- { label: 名称(无障碍文字), name: label, widget: string }
- { label: 链接, name: url, widget: string }
- label: 图标
name: icon
widget: select
required: false
options: [github, rss, email, bilibili, x]
hint: '不在列表里的留空 → 显示纯文字按钮'
- name: about
label: 关于页
file: content/site/about.yaml
format: yaml
# 块内图片上传落 content/site/assets/,yaml 写 ./assets/xxx(Astro 图片管线)
media_folder: content/site/assets
public_folder: ./assets
fields:
- label: 页面块(拖拽排序;每块可折叠)
name: blocks
widget: list
typeKey: type
required: false
summary: '{{fields.type}}'
hint: '点「+ 添加」选块型。关掉某块用该块内的「显示」开关'
types:
- label: 正文(markdown)
name: prose
widget: object
hint: markdown 正文块;可选小标题与版心/通栏宽度,长文分段改内容用
fields:
- { label: 显示, name: enabled, widget: boolean, required: false, default: true }
- label: 小标题(可选)
name: title
widget: object
required: false
fields:
- { label: 中文, name: zh, widget: string, required: false }
- { label: 英文, name: en, widget: string, required: false }
- label: 正文(markdown)
name: body
widget: object
fields:
- { label: 中文, name: zh, widget: markdown, required: false }
- { label: 英文, name: en, widget: markdown, required: false }
- label: 宽度
name: width
widget: select
required: false
default: narrow
options: [{ label: 版心宽度, value: narrow }, { label: 通栏, value: full }]
- label: 版图(图标+名称+描述)
name: tracks
widget: object
hint: 版图块:图标 + 名称 + 描述,适合「几条主线/能力面」式介绍
fields:
- { label: 显示, name: enabled, widget: boolean, required: false, default: true }
- label: 标题
name: title
widget: object
fields:
- { label: 中文, name: zh, widget: string, required: false }
- { label: 英文, name: en, widget: string, required: false }
- label: 条目
name: items
widget: list
summary: '{{fields.name.zh}}'
fields:
- label: 图标
name: icon
widget: select
required: false
options: [book, terminal, camera, code, git, rss, email, github, bilibili, x, star, link]
hint: 白名单外或留空 → 显示首字占位
- label: 名称
name: name
widget: object
fields:
- { label: 中文, name: zh, widget: string, required: false }
- { label: 英文, name: en, widget: string, required: false }
- label: 描述
name: desc
widget: object
required: false
fields:
- { label: 中文, name: zh, widget: text, required: false }
- { label: 英文, name: en, widget: text, required: false }
- label: 卡片(2/3 列)
name: cards
widget: object
hint: 卡片墙(2/3 列):每卡 = 标题 + 描述 + 可选链接/角标
fields:
- { label: 显示, name: enabled, widget: boolean, required: false, default: true }
- label: 标题(可选)
name: title
widget: object
required: false
fields:
- { label: 中文, name: zh, widget: string, required: false }
- { label: 英文, name: en, widget: string, required: false }
- { label: 列数, name: columns, widget: number, required: false, default: 3, min: 2, max: 3, value_type: int }
- label: 卡片条目
name: items
widget: list
summary: '{{fields.title.zh}}'
fields:
- label: 标题
name: title
widget: object
fields:
- { label: 中文, name: zh, widget: string, required: false }
- { label: 英文, name: en, widget: string, required: false }
- label: 描述
name: desc
widget: object
required: false
fields:
- { label: 中文, name: zh, widget: text, required: false }
- { label: 英文, name: en, widget: text, required: false }
- { label: 链接(可选), name: href, widget: string, required: false }
- label: 角标(可选)
name: tag
widget: object
required: false
fields:
- { label: 中文, name: zh, widget: string, required: false }
- { label: 英文, name: en, widget: string, required: false }
- label: 时间线
name: timeline
widget: object
hint: 时间线:一条时间 + 标题 + 描述;时间可写自由文本(如 2024 — 2026)
fields:
- { label: 显示, name: enabled, widget: boolean, required: false, default: true }
- label: 标题(可选)
name: title
widget: object
required: false
fields:
- { label: 中文, name: zh, widget: string, required: false }
- { label: 英文, name: en, widget: string, required: false }
- label: 条目
name: items
widget: list
summary: '{{fields.time}} · {{fields.title.zh}}'
fields:
- { label: 时间, name: time, widget: string, hint: '自由文本,如 2024 — 2026' }
- label: 标题
name: title
widget: object
fields:
- { label: 中文, name: zh, widget: string, required: false }
- { label: 英文, name: en, widget: string, required: false }
- label: 描述(可选)
name: desc
widget: object
required: false
fields:
- { label: 中文, name: zh, widget: text, required: false }
- { label: 英文, name: en, widget: text, required: false }
- label: 引文
name: quote
widget: object
hint: 单句引文 + 可选署名;配 font-kai 排版效果更好
fields:
- { label: 显示, name: enabled, widget: boolean, required: false, default: true }
- label: 引文
name: text
widget: object
fields:
- { label: 中文, name: zh, widget: text, required: false }
- { label: 英文, name: en, widget: text, required: false }
- label: 出处(可选)
name: cite
widget: object
required: false
fields:
- { label: 中文, name: zh, widget: string, required: false }
- { label: 英文, name: en, widget: string, required: false }
- label: 图片
name: figure
widget: object
hint: 单图 + 可选说明;图片只能后台上传(落 content/site/assets),禁外链
fields:
- { label: 显示, name: enabled, widget: boolean, required: false, default: true }
- { label: 图片(必填,请用后台上传), name: image, widget: image, hint: '禁止填外链:只能上传(落 content/site/assets/),走 Astro 图片管线' }
- { label: 替代文本(无障碍), name: alt, widget: string, required: false }
- label: 图注(可选)
name: caption
widget: object
required: false
fields:
- { label: 中文, name: zh, widget: string, required: false }
- { label: 英文, name: en, widget: string, required: false }
- label: 链接清单
name: linklist
widget: object
hint: 链接清单(标签 + 链接 + 备注),适合「相关阅读 / 外部资源」
fields:
- { label: 显示, name: enabled, widget: boolean, required: false, default: true }
- label: 标题(可选)
name: title
widget: object
required: false
fields:
- { label: 中文, name: zh, widget: string, required: false }
- { label: 英文, name: en, widget: string, required: false }
- label: 链接条目
name: items
widget: list
summary: '{{fields.label.zh}} → {{fields.href}}'
fields:
- label: 名称
name: label
widget: object
fields:
- { label: 中文, name: zh, widget: string, required: false }
- { label: 英文, name: en, widget: string, required: false }
- { label: 链接, name: href, widget: string }
- label: 备注(可选)
name: note
widget: object
required: false
fields:
- { label: 中文, name: zh, widget: string, required: false }
- { label: 英文, name: en, widget: string, required: false }
- label: 行动按钮
name: cta
widget: object
hint: 行动按钮(文案 + 链接 + 按钮字);一页建议不超过一个
fields:
- { label: 显示, name: enabled, widget: boolean, required: false, default: true }
- label: 说明文字
name: text
widget: object
fields:
- { label: 中文, name: zh, widget: string, required: false }
- { label: 英文, name: en, widget: string, required: false }
- { label: 按钮链接, name: href, widget: string }
- label: 按钮文字
name: label
widget: object
fields:
- { label: 中文, name: zh, widget: string, required: false }
- { label: 英文, name: en, widget: string, required: false }
- label: 分隔
name: divider
widget: object
hint: 分隔块:细分隔线或纯留白,给长页分段
fields:
- { label: 显示, name: enabled, widget: boolean, required: false, default: true }
- label: 样式
name: style
widget: select
required: false
default: line
options: [{ label: 细分隔线, value: line }, { label: 纯留白, value: space }]
- label: 名片区开关
name: layout
widget: object
required: false
hint: 关掉则不显示头像/名字/社交(它们的数据在「主页设置」)
fields:
- { label: 显示名片区, name: showIdCard, widget: boolean, required: false, default: true }
- label: 正文回退(blocks 为空时用,markdown,多段)
name: intro
widget: object
required: false
hint: 仅在「页面块」为空时渲染;与块同时存在时以块为准
fields:
- { label: 中文, name: zh, widget: markdown, required: false }
- { label: 英文, name: en, widget: markdown, required: false }
- label: 页面排版覆盖(可选)
name: typography
widget: object
required: false
hint: 本页正文默认字体 / 强调色 / 字号缩放(0.8–1.25);不填用全站默认,只影响本页
fields:
- label: 正文字体类
name: font
widget: select
required: false
options: [font-serif, font-kai, font-sans, font-mono, font-song, font-kai-sys, font-hei-sys, font-fangsong, font-huiwen, font-kuaile, font-xiaowei, font-huangyou, font-mashanzheng, font-liujianmaocao, font-smiley, font-neoxihei]
- label: 强调色类
name: accent
widget: select
required: false
options: [ink-accent, ink-accent-soft, c-blue, c-blue-soft, c-teal, c-teal-soft, c-green, c-green-soft, c-yellow, c-yellow-soft, c-orange, c-orange-soft, c-red, c-red-soft, c-pink, c-pink-soft, c-purple, c-purple-soft]
- { label: 字号缩放, name: scale, widget: number, required: false, value_type: float, min: 0.8, max: 1.25, step: 0.05 }
- name: opensource
label: 开源页
file: content/site/opensource.yaml
format: yaml
fields:
- label: 打字机动效文案(可选,≤6 条)
name: typing
widget: list
required: false
summary: '{{fields.zh}}'
hint: '档案卡顶部逐字打出的几行文案,2–6 条最佳;留空用内置默认。中英各一行,切换语言同步'
fields:
- { label: 中文, name: zh, widget: string }
- { label: English, name: en, widget: string }
- label: 叙述(链接前)
name: blurb
widget: object
required: false
hint: 富文本(markdown + 排版类),渲染在仓库链接之前
fields:
- { label: 中文, name: zh, widget: string, required: false }
- { label: 英文, name: en, widget: string, required: false }
- label: 页面排版覆盖(可选)
name: typography
widget: object
required: false
hint: 本页正文默认字体 / 强调色 / 字号缩放(0.8–1.25);不填用全站默认,只影响本页
fields:
- label: 正文字体类
name: font
widget: select
required: false
hint: 整页正文换字族;艺术/展示类字族体积大。5i 新增六款(小薇/庆科黄油/马善政/柳建毛草/得意黑/新晰黑)均按站内用字切片,单页增量 27–46KB;站外用字(生僻字)会回落系统字族,需按 docs 里的命令重切
options: [font-serif, font-kai, font-sans, font-mono, font-song, font-kai-sys, font-hei-sys, font-fangsong, font-huiwen, font-kuaile, font-xiaowei, font-huangyou, font-mashanzheng, font-liujianmaocao, font-smiley, font-neoxihei]
- label: 强调色类
name: accent
widget: select
required: false
options: [ink-accent, ink-accent-soft, c-blue, c-blue-soft, c-teal, c-teal-soft, c-green, c-green-soft, c-yellow, c-yellow-soft, c-orange, c-orange-soft, c-red, c-red-soft, c-pink, c-pink-soft, c-purple, c-purple-soft]
- { label: 字号缩放, name: scale, widget: number, required: false, value_type: float, min: 0.8, max: 1.25, step: 0.05, hint: '0.8–1.25;1 = 全站默认' }
- { label: 仓库链接地址, name: repo_url, widget: string, required: false }
- { label: 仓库链接文字, name: repo_label, widget: string, required: false }
- label: 叙述(链接后)
name: blurb_after
widget: object
fields:
- { label: 中文, name: zh, widget: text, required: false }
- { label: 英文, name: en, widget: text, required: false }
- label: 置顶项目(仓库短名,顺序即展示顺序)
name: pinned
widget: list
required: false
hint: '如 blog;留空则回落默认 blog。填写的名字须与仓库短名一致'
- label: 项目墙模式
name: mode
widget: select
required: false
default: all
options: [{ label: 全部, value: all }, { label: 仅白名单, value: only }, { label: 排除黑名单, value: exclude }]
hint: only/exclude 需配合下面的名单
- label: 白名单(仓库短名)
name: include
widget: list
required: false
hint: 'mode=仅白名单时生效,如 pi-board'
- label: 黑名单(仓库短名)
name: exclude
widget: list
required: false
hint: 'mode=排除黑名单时生效,如 blog'
- label: 排序
name: sort
widget: select
required: false
default: pinned-first
options:
- { label: 置顶优先,其余按星数, value: pinned-first }
- { label: 星数降序, value: stars }
- { label: 最近更新, value: updated }
- label: 来源过滤
name: perSource
widget: select
required: false
default: both
options: [{ label: 双源合并, value: both }, { label: 仅 GitHub, value: github }, { label: 仅自托管 gitea, value: gitea }]
- label: 开篇文案来源
name: intro_from
widget: select
required: false
default: 'off'
options:
- { label: 用本页叙述(默认), value: 'off' }
- { label: GitHub profile README, value: github-readme }
hint: 'github-readme = 构建期取 wait4xx/wait4xx 的 README 渲染为开篇;缓存入仓 content/.github-readme.md,取不到自动回落叙述'
+3
View File
@@ -8,5 +8,8 @@
<body> <body>
<!-- pinned 本地 bundle(0.252.0):官方模板仅此一个 script,无 CSS link、无 type=module --> <!-- pinned 本地 bundle(0.252.0):官方模板仅此一个 script,无 CSS link、无 type=module -->
<script src="./sveltia-cms.js"></script> <script src="./sveltia-cms.js"></script>
<!-- 可视化排版选择:类清单(单一数据源)→ 编辑器组件注册(须在 CMS 之后、init 之前注册) -->
<script src="./typography-classes.js"></script>
<script src="./typography-editor.js"></script>
</body> </body>
</html> </html>
+57
View File
@@ -0,0 +1,57 @@
/**
* 排版类清单 —— 单一事实源(后台编辑器面板与文档共用)。
* 与 src/styles/global.css 的「受限排版类/语义色板」段、src/lib/rehype-class-whitelist.ts 的白名单三处保持一致:
* 加类时三处同改,否则构建期白名单会把新类剥掉。
*
* swatch / family 仅供**后台面板预览**使用(后台无站点 CSS 变量,故给具体值):
* swatch 取亮态值(后台是浅色 UI),family 为该类的真实字体栈。
*/
window.TYPOGRAPHY_CLASSES = {
font: [
{ cls: 'font-serif', label: '思源宋(书评正文)', family: '"Noto Serif SC", Georgia, serif', kind: '自托管' },
{ cls: 'font-kai', label: '霞鹜文楷(金句)', family: '"LXGW WenKai Screen", "Songti SC", serif', kind: '自托管' },
{ cls: 'font-sans', label: '思源黑(UI/技术)', family: '"Noto Sans SC", system-ui, sans-serif', kind: '自托管' },
{ cls: 'font-mono', label: 'JetBrains Mono(等宽)', family: '"JetBrains Mono", ui-monospace, monospace', kind: '自托管' },
{ cls: 'font-song', label: '宋体族', family: '"Noto Serif SC", "Songti SC", SimSun, serif', kind: '自托管优先' },
{ cls: 'font-kai-sys', label: '楷体族', family: '"LXGW WenKai Screen", "Kaiti SC", STKaiti, serif', kind: '自托管优先' },
{ cls: 'font-hei-sys', label: '黑体族', family: '"Noto Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif', kind: '自托管优先' },
{ cls: 'font-fangsong', label: '仿宋族', family: 'FangSong, STFangsong, serif', kind: '系统回退' },
{ cls: 'font-huiwen', label: '汇文明朝体', family: '"WF-Hwmc", serif', kind: '自托管' },
{ cls: 'font-kuaile', label: '站酷快乐体', family: '"ZCOOL KuaiLe", serif', kind: '自托管' },
{ cls: 'font-art-kuaile', label: '快乐体(艺术·仅标题)', family: '"ZCOOL KuaiLe", serif', kind: '艺术字' },
// 5i T9 五款字族扩展(2026-11-03,均 OFL / 免费商用且允许 web 嵌入)
{ cls: 'font-xiaowei', label: '站酷小薇(文学·宋)', family: '"ZCOOL XiaoWei", serif', kind: '自托管' },
{ cls: 'font-huangyou', label: '站酷庆科黄油(标题)', family: '"ZCOOL QingKe HuangYou", sans-serif', kind: '自托管' },
{ cls: 'font-mashanzheng', label: '马善政(书法)', family: '"Ma Shan Zheng", "Kaiti SC", serif', kind: '自托管' },
{ cls: 'font-liujianmaocao', label: '柳建毛草(草书)', family: '"Liu Jian Mao Cao", "Kaiti SC", cursive', kind: '自托管' },
{ cls: 'font-smiley', label: '得意黑(斜体标题)', family: '"Smiley Sans", sans-serif', kind: '自托管' },
{ cls: 'font-neoxihei', label: '霞鹜新晰黑(正文黑)', family: '"LXGW Neo XiHei", "Noto Sans SC", sans-serif', kind: '自托管' },
],
color: [
{ cls: 'ink-1', label: '最重(标题级)', swatch: '#111113' },
{ cls: 'ink-2', label: '正文', swatch: '#33343a' },
{ cls: 'ink-3', label: '次要(日期/元信息)', swatch: '#6b6e78' },
{ cls: 'ink-muted', label: '最淡(注释感)', swatch: '#70747c' },
{ cls: 'ink-warm', label: '暖调正文', swatch: '#3f362e' },
{ cls: 'ink-cool', label: '冷调正文', swatch: '#2f3742' },
{ cls: 'ink-faint', label: '最淡可读档', swatch: '#70747c' },
{ cls: 'ink-accent', label: '强调(链接色)', swatch: '#3454d1' },
{ cls: 'ink-accent-soft', label: '弱强调', swatch: '#4f66c2' },
{ cls: 'c-blue', label: '蓝', swatch: '#2456c9' },
{ cls: 'c-blue-soft', label: '蓝(soft)', swatch: '#4170dd' },
{ cls: 'c-teal', label: '青', swatch: '#0d7d74' },
{ cls: 'c-teal-soft', label: '青(soft)', swatch: '#0e847a' },
{ cls: 'c-green', label: '绿', swatch: '#1f7a3a' },
{ cls: 'c-green-soft', label: '绿(soft)', swatch: '#22853f' },
{ cls: 'c-yellow', label: '黄(已压暗)', swatch: '#7a5900' },
{ cls: 'c-yellow-soft', label: '黄(soft)', swatch: '#926a00' },
{ cls: 'c-orange', label: '橙', swatch: '#9a4a00' },
{ cls: 'c-orange-soft', label: '橙(soft)', swatch: '#ba5900' },
{ cls: 'c-red', label: '红', swatch: '#b32020' },
{ cls: 'c-red-soft', label: '红(soft)', swatch: '#db3737' },
{ cls: 'c-pink', label: '粉', swatch: '#a3246a' },
{ cls: 'c-pink-soft', label: '粉(soft)', swatch: '#ce3386' },
{ cls: 'c-purple', label: '紫', swatch: '#6a35b8' },
{ cls: 'c-purple-soft', label: '紫(soft)', swatch: '#8c5ed1' },
],
};
+153
View File
@@ -0,0 +1,153 @@
/**
* 后台可视化排版选择(Sveltia CMS 自定义编辑器组件)。
*
* 能力依据(sveltiacms.app/en/docs/api/editor-components,2026-11 核实):
* - inline 型组件(type:'inline')在段落内以 pattern 匹配,可与选中文字互转;
* - onInsert({ selectedText }) 拿到编辑器当前选中的文字,返回值经 toInline 生成 Markdown;
* - inline 组件固定在 Insert 菜单下(trigger 对 inline 是文本触发,不能上工具栏)。
*
* 因此「选中文字 → 套字体/颜色」由**一个「排版」组件 + 自建分组面板**实现:
* 两个独立工具栏下拉(字体/颜色)会共用同一 span pattern 互相抢匹配,这里收成一个入口分两组,
* 交互上等价(选中 → Insert ▸ 排版 → 选类 → 文字被 <span class> 包住)。
* onEdit 支持点开已套用的片段改类。
*/
(function () {
const CLASSES = window.TYPOGRAPHY_CLASSES;
if (!CLASSES || !window.CMS) return;
/** 自建选择面板:返回 Promise<选中类名 | null>;无依赖、可键盘操作 */
function pickClass(current) {
return new Promise((resolve) => {
const wrap = document.createElement('div');
wrap.setAttribute('role', 'dialog');
wrap.setAttribute('aria-label', '选择排版');
Object.assign(wrap.style, {
position: 'fixed', inset: '0', zIndex: '99999', display: 'flex',
alignItems: 'center', justifyContent: 'center', background: 'rgba(8,10,14,.55)',
});
const card = document.createElement('div');
Object.assign(card.style, {
background: '#fff', color: '#111113', borderRadius: '12px', padding: '18px 20px',
width: 'min(560px, 92vw)', maxHeight: '80vh', overflowY: 'auto',
boxShadow: '0 8px 32px rgba(0,0,0,.28)', font: '14px/1.5 system-ui, sans-serif',
});
const title = document.createElement('p');
title.textContent = '选择排版(作用于选中文字)';
Object.assign(title.style, { margin: '0 0 12px', fontWeight: '600' });
card.appendChild(title);
const buttons = [];
const addGroup = (name, list, withSwatch) => {
const h = document.createElement('p');
h.textContent = name;
Object.assign(h.style, { margin: '12px 0 6px', fontSize: '12px', color: '#6b6e78' });
card.appendChild(h);
const row = document.createElement('div');
Object.assign(row.style, { display: 'flex', flexWrap: 'wrap', gap: '8px' });
for (const item of list) {
const b = document.createElement('button');
b.type = 'button';
b.dataset.cls = item.cls;
b.className = item.cls === current ? 'active' : '';
Object.assign(b.style, {
display: 'inline-flex', alignItems: 'center', gap: '8px',
padding: '6px 12px', borderRadius: '8px', cursor: 'pointer',
border: '1px solid ' + (item.cls === current ? '#3454d1' : '#e6e7eb'),
background: item.cls === current ? '#eef1fd' : '#fff',
color: '#111113', fontSize: '13px',
});
if (withSwatch) {
// 色块预览:直接展示该类在亮态下的实际颜色
const chip = document.createElement('span');
Object.assign(chip.style, {
width: '16px', height: '16px', borderRadius: '4px', flex: 'none',
background: item.swatch || '#ccc',
border: '1px solid rgba(0,0,0,.12)',
});
chip.setAttribute('aria-hidden', 'true');
b.appendChild(chip);
} else {
// 字体项:名称 + 用该字体栈渲染的示例字形
const sample = document.createElement('span');
sample.textContent = '秋海';
Object.assign(sample.style, {
fontFamily: item.family || 'inherit', fontSize: '15px', lineHeight: '1',
padding: '0 4px', borderLeft: '1px solid #e6e7eb', color: '#33343a',
});
b.appendChild(sample);
}
const label = document.createElement('span');
label.textContent = item.label + (item.kind ? `(${item.kind})` : '');
b.appendChild(label);
row.appendChild(b);
buttons.push(b);
}
card.appendChild(row);
};
addGroup('字体', CLASSES.font, false);
addGroup('颜色', CLASSES.color, true);
const foot = document.createElement('div');
Object.assign(foot.style, { marginTop: '16px', display: 'flex', justifyContent: 'flex-end', gap: '8px' });
const cancel = document.createElement('button');
cancel.type = 'button';
cancel.textContent = '取消(Esc)';
Object.assign(cancel.style, { padding: '6px 12px', borderRadius: '8px', border: '1px solid #e6e7eb', background: '#fff', cursor: 'pointer' });
foot.appendChild(cancel);
card.appendChild(foot);
wrap.appendChild(card);
const prevFocus = document.activeElement;
const close = (val) => {
document.removeEventListener('keydown', onKey, true);
wrap.remove();
if (prevFocus && prevFocus.focus) prevFocus.focus();
resolve(val);
};
const onKey = (e) => {
if (e.key === 'Escape') {
e.preventDefault();
close(null);
} else if (e.key === 'Tab') {
// 焦点循环留在面板内
const list = [...wrap.querySelectorAll('button')];
const first = list[0], last = list[list.length - 1];
if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); }
else if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); }
}
};
wrap.addEventListener('click', (e) => {
const t = e.target;
if (t === wrap) return close(null);
if (t.dataset && t.dataset.cls) close(t.dataset.cls);
if (t === cancel) close(null);
});
document.addEventListener('keydown', onKey, true);
document.body.appendChild(wrap);
(buttons[0] || cancel).focus();
});
}
window.CMS.registerEditorComponent({
id: 'typography',
label: '排版(字体/颜色)',
icon: 'format_paint',
type: 'inline',
pattern: /<span class="(?<cls>[a-z0-9-]+)">(?<text>[^<]*?)<\/span>/,
toInline: ({ cls = '', text = '' }) => `<span class="${cls}">${text}</span>`,
toPreview: ({ cls = '', text = '' }) => {
const span = document.createElement('span');
span.className = cls;
span.textContent = text; // DOM 预览:textContent 天然转义
return span;
},
onInsert: async ({ selectedText = '' }) => {
const cls = await pickClass(null);
return cls ? { cls, text: selectedText } : null;
},
onEdit: async ({ data }) => {
const cls = await pickClass(data.cls);
return cls ? { ...data, cls } : null;
},
});
})();
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loaded 100 of 1223 files, more files were not shown because too many files have changed in this diff. Show more