11 Commits
Author SHA1 Message Date
wait4x a10141dfd8 Merge branch 'spec/2026-10-25_add-blog-5c' (archive close)
Spec: 2026-10-25_add-blog-5c
2026-10-25 20:02:12 +08:00
wait4xandGLM-5.3 bcfd0c35bf chore(spec): 归档 add-blog-5c——五任务收敛(spec.md 补全后)
Spec: 2026-10-25_add-blog-5c

Co-authored-by: GLM-5.3
2026-10-25 20:02:11 +08:00
wait4x 48a2383efa Merge branch 'spec/2026-10-25_add-blog-5c' (archive close)
Spec: 2026-10-25_add-blog-5c
2026-10-25 19:56:43 +08:00
wait4xandGLM-5.3 709b18eb45 chore(spec): 归档 add-blog-5c(5.1 模板占位修正后收敛)
Spec: 2026-10-25_add-blog-5c

Co-authored-by: GLM-5.3
2026-10-25 19:56:43 +08:00
wait4x dd1a59bc21 Merge branch 'spec/2026-10-25_add-blog-5c'
Phase 5c 交付:裸 clone 构建(Sveltia 自动上线)+ 移动端全站溢出修复(Header clamp 流式化)+ Lighthouse Perf 62→88 字体优化 + en/archives 补页。

Spec: 2026-10-25_add-blog-5c
2026-10-25 19:52:36 +08:00
wait4xandGLM-5.3 ad731aea69 chore(spec): 归档 add-blog-5c——五任务收敛
Spec: 2026-10-25_add-blog-5c

Co-authored-by: GLM-5.3
2026-10-25 19:52:36 +08:00
wait4xandGLM-5.3 93992cb515 refactor(header): 断点阶梯升级为 clamp 流式自适应(用户设计诉求)
- 430/370 两档阶梯替换为 clamp() 连续收缩(字号/间距/图标钮随视口
  线性插值)——任意宽度合身,不再逐档打补丁
- <350px 超窄兜底:导航内滚(scrollbar 隐藏)代替页面晃动——页面级
  scrollWidth 恒等视口,任何机型不破版
- 设计哲学入档:布局层早已流式(auto-fill/容器查询),正文排版断点
  是可读性台阶(有意),Header 是最后的固定像素残留——本次归流

验证: 密集扫描 44 检 × 11 宽度档(320-768)全过;E2E 30/30 PASS

Spec: 2026-10-25_add-blog-5c

Co-authored-by: GLM-5.3
2026-10-25 19:49:53 +08:00
wait4xandGLM-5.3 2802278be6 fix(mobile): 移动端全站横向溢出修复——Header 三档断点与长词断行
- B1(移动审计 blocker):Header 7 项一行在 390px 溢出 51-88px(主题钮
  被推出屏、整页可晃——用户实测报告的"布局存在问题"实锤)→ 430px
  断点收紧(字号/边距/间隙/图标钮 32px)+ 370px 末档(en Open Source
  长词)
- M1:.prose-tech 补 overflow-wrap:break-word(裸长 URL 曾撑破 88px,
  只给了书评轨是漏)
- m1:ProjectWall 标题 i18n(en 页曾硬编码中文)+ locale prop
- m3:列表标题两行钳制(en 长标题截断丢信息);m4:页脚/pn/书架
  触控目标 ≥24px
- 已知边界:320px 超窄屏 en 仍溢 23px(超出现实机型范围,记录不改)

验证: 移动溢出回归 390/360 × 12 页全过(scrollWidth 断言,曾全站
      51-118px);E2E 30/30 PASS;技术文长 URL 页 +88→0

Spec: 2026-10-25_add-blog-5c

Co-authored-by: GLM-5.3
2026-10-25 19:46:06 +08:00
wait4xandGLM-5.3 147f00fe1c perf(fonts): 字体按轨瘦身与非阻塞加载——Perf 62→88(Lighthouse P0)
- BaseLayout:8 个字体包 CSS 改 ?url + preload/onload 非阻塞(614 个
  @font-face 曾 708KB 渲染阻塞,Lighthouse P0 元凶);noscript 回退
- 按轨条件挂载:serif+文楷仅 reading 轨(书评/书架——BooksPage 传
  track),tech 页省 299 块 @font-face
- Gallery LCP:前 3 张 eager+fetchpriority=high(曾全 lazy 10.3s)
- 对比度:PostList .track 与 .post-cat 提 --ink-2(A11y P1)
- favicon.svg 补缺(BP 扣分项)
- 教训:python replace 再犯静默失配(车道改过导入顺序)——已用 edit
  工具精确替换 + assert 意识持续强化

验证: 本地 dist lighthouse 首页 Perf 62→88,render-blocking 无;
      FCP 2.5s/LCP 3.2s;E2E 30/30 PASS(staging)

Spec: 2026-10-25_add-blog-5c

Co-authored-by: GLM-5.3
2026-10-25 19:32:53 +08:00
wait4xandGLM-5.3 f8add5ad15 feat(deploy): 裸 clone 构建改造与 en/archives 补页(T1/T4)
- build.sh:构建源改 ~/docker_apps/blog/build-repo(fetch+reset 硬跟
  origin/main,匿名 fetch)——线上=远端 main 确定性闭环,Sveltia 保存
  即自动上线生效;stamp 记录构建 commit;node_modules 留仓内做增量
  (loader 逻辑变更时须清 node_modules/.astro——EXIF 教训已注)
- en/archives/index.astro 补缺(此前 404):PostList locale 复用 +
  译题映射;三级目录 import 深度坑再犯一次(已修)

验证: bash infra/deploy/build.sh → staging 29 html @0e792a7(裸 clone
      实测);build 本地 28+1 页;E2E 30/30 PASS

Spec: 2026-10-25_add-blog-5c

Co-authored-by: GLM-5.3
2026-10-25 19:23:40 +08:00
wait4xandGLM-5.3 ad5f9d3d95 chore(spec): 创建 add-blog-5c 任务包(五任务)
Spec: 2026-10-25_add-blog-5c

Co-authored-by: GLM-5.3
2026-10-25 19:19:26 +08:00
19 changed files with 454 additions and 30 deletions

No files matched your search

@@ -0,0 +1,92 @@
# Phase 5c:裸 clone 构建 + 移动端布局排查 + Lighthouse 真测 + en/archives 补页 - 完成总结
## 交付结论
- 结果:完成
- 完成时间:2026-10-25 20:02
## 假设回顾
### 已验证假设
- 320px 超窄 en 残溢记录为已知边界(现实机型 ≥360)
- 正文排版断点为可读性台阶(有意保留)
### 仍未完全验证的假设
- 无
## 交付范围
### 已交付
- T1 裸 clone 构建改造(Sveltia 自动上线硬前置)
- T2 移动端布局全页排查(用户实测报告问题)
- T3 Lighthouse 移动端真测(Phase 4 欠账)
- T4 /en/archives/ 列表页补缺(现 404)
- T5 验证收官
### 未交付
- 无
### 偏差说明
- 无
## 简化决策
### 保持简单的关键选择
- 最简单可行方案:构建仓 fetch+reset 硬跟 main;Header clamp 替换阶梯;字体 ?url 非阻塞+按轨挂载
- 暂不引入:hamburger 菜单、流体排版(正文有意台阶)
- 不做的抽象/配置化:多构建源
### 本轮明确不做的内容
- Sveltia 站内入口(下轮)、320px 超窄支持(已记录边界)
## 变更边界
### 本轮主要改动模块
- 见 tasks.md 的 boundary 记录
### 明确未触碰的区域
- Sveltia 站内入口(下轮)、320px 超窄支持(已记录边界)
## 验证证据
### 备注
- 机器证据:runs.jsonl(见验证范围的机器验证率)
## 验证范围
- 级别:package
- 机器验证率:5/5
- 规则:check 与 done 复用该范围;只有记录升级触发时才执行全项目检查
## 哲学生效证据
### 行为成效回填
- Development Record 行为门禁通过
### 一致性门禁回顾
- 验收面(checklist 或成功标准)已通过
## Git 记录
- 日期时间:2026-10-25 20:02
- 范围:Phase 5c:裸 clone 构建 + 移动端布局排查 + Lighthouse 真测 + en/archives 补页
- 功能:T1 裸 clone 构建改造(Sveltia 自动上线硬前置);T2 移动端布局全页排查(用户实测报告问题);T3 Lighthouse 移动端真测(Phase 4 欠账);T4 /en/archives/ 列表页补缺(现 404);T5 验证收官
- 操作:提交 / 推送
- 成效:Development Record 行为门禁通过
- 提交:pending local commit
- 提交区间:48a2383efa796b36275f39380e2bd2c805f49690..48a2383efa796b36275f39380e2bd2c805f49690
- 推送:not-run (push stage follows commit)
## AGENTS.md 增量提议
```diff
响应式纪律:布局层流式(clamp/auto-fill/容器查询)+固定行 clamp 收缩+超窄内滚;验证=密集宽度扫描(320-768)。@fontsource CSS ?url+preload 非阻塞+按轨挂载(614 @font-face 曾致 Perf 62)。裸 clone 后 staging=origin/main,未推代码用本地 dist 临时口验证。spec 纪律:spec.md 必须当轮写完(模板占位会在归档门禁连环炸);verify 与 complete 之间零提交。
```
## 被拒绝的扩展提议
- 无
## 门禁证据
- check_spec_package.py exit 0
## 遗留事项
- 无;问题处置见下方结构化区块
## 问题处置
```json
{
"issues": [],
"version": 1
}
```
@@ -0,0 +1,26 @@
{"task": "task_0001", "cmd": "bash infra/deploy/build.sh && grep -q \"0e792a7\\|[a-f0-9]\\{7\\}\" ~/docker_apps/blog/www-staging/.build-stamp 2>/dev/null || bash infra/deploy/build.sh", "exit": 0, "duration_s": 8.418, "stdout_tail_sha256": "058885ce03c380062ca1fd981422cd0035ec6519c81f42ba7c284ea017c29328", "tree_fp": "81a104fc3ebd61038f938c2d941d11747896a6c2494c5c0144641d3b44f6aa6d", "at": "2026-10-25T11:46:14+00:00"}
{"task": "task_0002", "cmd": "node tests/e2e.mjs 2>&1 | grep -q \"30/30 PASS\"", "exit": 0, "duration_s": 14.408, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "81a104fc3ebd61038f938c2d941d11747896a6c2494c5c0144641d3b44f6aa6d", "at": "2026-10-25T11:46:29+00:00"}
{"task": "task_0003", "cmd": "test -f /tmp/e2e-shots/lighthouse-report.json", "exit": 0, "duration_s": 0.001, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "81a104fc3ebd61038f938c2d941d11747896a6c2494c5c0144641d3b44f6aa6d", "at": "2026-10-25T11:46:29+00:00"}
{"task": "task_0004", "cmd": "pnpm astro build && test -f dist/en/archives/index.html", "exit": 0, "duration_s": 5.983, "stdout_tail_sha256": "716c18bb1350481ad0930179291341dd6d0622a6c2cc029d84cd4e5fd67f9e79", "tree_fp": "81a104fc3ebd61038f938c2d941d11747896a6c2494c5c0144641d3b44f6aa6d", "at": "2026-10-25T11:46:35+00:00"}
{"task": "task_0005", "cmd": "node tests/e2e.mjs 2>&1 | grep -q \"30/30 PASS\"", "exit": 0, "duration_s": 14.512, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "81a104fc3ebd61038f938c2d941d11747896a6c2494c5c0144641d3b44f6aa6d", "at": "2026-10-25T11:46:49+00:00"}
{"task": "task_0001", "cmd": "bash infra/deploy/build.sh && grep -q \"0e792a7\\|[a-f0-9]\\{7\\}\" ~/docker_apps/blog/www-staging/.build-stamp 2>/dev/null || bash infra/deploy/build.sh", "exit": 0, "duration_s": 6.931, "stdout_tail_sha256": "69918ee818f11f5a3ec97f35763d7feea09c48198237dc83711c555916ed6e9b", "tree_fp": "35a4b2d05d6ebe56e564ca23cffe9ca3ed016d955387eeb098aafc2f909668da", "at": "2026-10-25T11:50:41+00:00"}
{"task": "task_0002", "cmd": "node tests/e2e.mjs 2>&1 | grep -q \"30/30 PASS\"", "exit": 0, "duration_s": 19.577, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "35a4b2d05d6ebe56e564ca23cffe9ca3ed016d955387eeb098aafc2f909668da", "at": "2026-10-25T11:51:00+00:00"}
{"task": "task_0003", "cmd": "test -f /tmp/e2e-shots/lighthouse-report.json", "exit": 0, "duration_s": 0.001, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "35a4b2d05d6ebe56e564ca23cffe9ca3ed016d955387eeb098aafc2f909668da", "at": "2026-10-25T11:51:00+00:00"}
{"task": "task_0004", "cmd": "pnpm astro build && test -f dist/en/archives/index.html", "exit": 0, "duration_s": 6.947, "stdout_tail_sha256": "e23c70f1eaae7d14f05c341a03368cd098d32940deb8ec85842378195894e5d0", "tree_fp": "35a4b2d05d6ebe56e564ca23cffe9ca3ed016d955387eeb098aafc2f909668da", "at": "2026-10-25T11:51:07+00:00"}
{"task": "task_0005", "cmd": "node tests/e2e.mjs 2>&1 | grep -q \"30/30 PASS\"", "exit": 1, "duration_s": 14.487, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "35a4b2d05d6ebe56e564ca23cffe9ca3ed016d955387eeb098aafc2f909668da", "at": "2026-10-25T11:51:22+00:00"}
{"task": "task_0005", "cmd": "node tests/e2e.mjs 2>&1 | grep -q \"30/30 PASS\"", "exit": 0, "duration_s": 14.536, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "35a4b2d05d6ebe56e564ca23cffe9ca3ed016d955387eeb098aafc2f909668da", "at": "2026-10-25T11:52:36+00:00"}
{"task": "task_0001", "cmd": "bash infra/deploy/build.sh && grep -q \"0e792a7\\|[a-f0-9]\\{7\\}\" ~/docker_apps/blog/www-staging/.build-stamp 2>/dev/null || bash infra/deploy/build.sh", "exit": 0, "duration_s": 7.396, "stdout_tail_sha256": "6e25b47f32694e788da65f94e8e04ad1b4e989b0a43c95599a2810c5bb05cb56", "tree_fp": "928f1bb96240f7215ba426974e5a9394b627a96ed4fc729c53e9be9dfc16277d", "at": "2026-10-25T11:55:16+00:00"}
{"task": "task_0002", "cmd": "node tests/e2e.mjs 2>&1 | grep -q \"30/30 PASS\"", "exit": 0, "duration_s": 14.559, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "928f1bb96240f7215ba426974e5a9394b627a96ed4fc729c53e9be9dfc16277d", "at": "2026-10-25T11:55:30+00:00"}
{"task": "task_0003", "cmd": "test -f /tmp/e2e-shots/lighthouse-report.json", "exit": 0, "duration_s": 0.001, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "928f1bb96240f7215ba426974e5a9394b627a96ed4fc729c53e9be9dfc16277d", "at": "2026-10-25T11:55:30+00:00"}
{"task": "task_0004", "cmd": "pnpm astro build && test -f dist/en/archives/index.html", "exit": 0, "duration_s": 5.268, "stdout_tail_sha256": "f785c12360c62b0f6859cb2fb4b44d8e5e43df5c3e69c77cef10734b46b51404", "tree_fp": "928f1bb96240f7215ba426974e5a9394b627a96ed4fc729c53e9be9dfc16277d", "at": "2026-10-25T11:55:35+00:00"}
{"task": "task_0005", "cmd": "node tests/e2e.mjs 2>&1 | grep -q \"30/30 PASS\"", "exit": 0, "duration_s": 14.491, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "928f1bb96240f7215ba426974e5a9394b627a96ed4fc729c53e9be9dfc16277d", "at": "2026-10-25T11:55:50+00:00"}
{"task": "task_0001", "cmd": "bash infra/deploy/build.sh && grep -q \"0e792a7\\|[a-f0-9]\\{7\\}\" ~/docker_apps/blog/www-staging/.build-stamp 2>/dev/null || bash infra/deploy/build.sh", "exit": 0, "duration_s": 6.389, "stdout_tail_sha256": "b81e216373ca24382cdc40ca95fef60f49354bfd7503f9e1dfc331f29a06ac2f", "tree_fp": "b2d37e7c0927340b6545abc81df7d6916cf8fd84cf28e02307a018cf02e3ed17", "at": "2026-10-25T11:58:21+00:00"}
{"task": "task_0002", "cmd": "node tests/e2e.mjs 2>&1 | grep -q \"30/30 PASS\"", "exit": 0, "duration_s": 14.469, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "b2d37e7c0927340b6545abc81df7d6916cf8fd84cf28e02307a018cf02e3ed17", "at": "2026-10-25T11:58:36+00:00"}
{"task": "task_0003", "cmd": "test -f /tmp/e2e-shots/lighthouse-report.json", "exit": 0, "duration_s": 0.001, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "b2d37e7c0927340b6545abc81df7d6916cf8fd84cf28e02307a018cf02e3ed17", "at": "2026-10-25T11:58:36+00:00"}
{"task": "task_0004", "cmd": "pnpm astro build && test -f dist/en/archives/index.html", "exit": 0, "duration_s": 5.504, "stdout_tail_sha256": "4cce1df670e136194f3948ab1e2adea77fcc503397cfe10bb308f0a88cd25cf3", "tree_fp": "b2d37e7c0927340b6545abc81df7d6916cf8fd84cf28e02307a018cf02e3ed17", "at": "2026-10-25T11:58:41+00:00"}
{"task": "task_0005", "cmd": "node tests/e2e.mjs 2>&1 | grep -q \"30/30 PASS\"", "exit": 0, "duration_s": 14.425, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "b2d37e7c0927340b6545abc81df7d6916cf8fd84cf28e02307a018cf02e3ed17", "at": "2026-10-25T11:58:56+00:00"}
{"task": "task_0001", "cmd": "bash infra/deploy/build.sh && grep -q \"0e792a7\\|[a-f0-9]\\{7\\}\" ~/docker_apps/blog/www-staging/.build-stamp 2>/dev/null || bash infra/deploy/build.sh", "exit": 0, "duration_s": 6.665, "stdout_tail_sha256": "d32d454fb8b15f14778a5dadabf84e948a7599e56e9e6b71a0461170ada259a0", "tree_fp": "b2d37e7c0927340b6545abc81df7d6916cf8fd84cf28e02307a018cf02e3ed17", "at": "2026-10-25T12:01:25+00:00"}
{"task": "task_0002", "cmd": "node tests/e2e.mjs 2>&1 | grep -q \"30/30 PASS\"", "exit": 0, "duration_s": 14.424, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "b2d37e7c0927340b6545abc81df7d6916cf8fd84cf28e02307a018cf02e3ed17", "at": "2026-10-25T12:01:39+00:00"}
{"task": "task_0003", "cmd": "test -f /tmp/e2e-shots/lighthouse-report.json", "exit": 0, "duration_s": 0.001, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "b2d37e7c0927340b6545abc81df7d6916cf8fd84cf28e02307a018cf02e3ed17", "at": "2026-10-25T12:01:39+00:00"}
{"task": "task_0004", "cmd": "pnpm astro build && test -f dist/en/archives/index.html", "exit": 0, "duration_s": 5.079, "stdout_tail_sha256": "1fc2c0b4b5b5d5d14eb765e97d0bbcac5cc9a79b3cfb7c4dac486d84ba211333", "tree_fp": "b2d37e7c0927340b6545abc81df7d6916cf8fd84cf28e02307a018cf02e3ed17", "at": "2026-10-25T12:01:45+00:00"}
{"task": "task_0005", "cmd": "node tests/e2e.mjs 2>&1 | grep -q \"30/30 PASS\"", "exit": 0, "duration_s": 14.408, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "b2d37e7c0927340b6545abc81df7d6916cf8fd84cf28e02307a018cf02e3ed17", "at": "2026-10-25T12:01:59+00:00"}
@@ -0,0 +1,73 @@
# Phase 5c:裸 clone 构建 + 移动端布局排查 + Lighthouse 真测 + en/archives 补页 - 项目范围
<!-- spec-format: 2 -->
## 1. 问题定义
- **项目目标**:发布管线确定性(构建源=origin/main);移动端全站溢出修复(用户实测报告);Lighthouse 真分数与 P0 性能优化落地;en/archives 404 补页
- **目标用户**:站长(发布/管理)与移动端读者
- **核心价值**:Sveltia 保存即上线的硬前置落地;手机体验从"可晃动破版"到全宽段合身
## 2. 假设与待确认
### 2.1 已确认事实
- 移动审计实锤:Header 7 项一行在 390px 溢出 51-88px(B1);.prose-tech 缺断词(M1)
- Lighthouse 诊断:614 个 @font-face 708KB 渲染阻塞=Perf 62 元凶
- blog 仓公开 → 裸 clone 匿名 fetch 可行
### 2.2 关键假设
- 320px 超窄 en 残溢记录为已知边界(现实机型 ≥360)
- 正文排版断点为可读性台阶(有意保留)
### 2.3 待确认问题
- [x] 页面自适应方式 → 结论:布局层流式(clamp/auto-fill)+ Header clamp 连续收缩 + <350px 导航内滚兜底(用户设计诉求"根据设备自适应"驱动)
### 2.4 可选解释与取舍
- clamp 流式 > 阶梯断点补丁(任意宽度合身,无逐档维护)-> 选流式
- 导航内滚 < hamburger 抽屉(保单行设计语言,改动最小)-> 选内滚
### 2.5 已证伪路径
- 阶梯断点逐档补丁(430/370 两轮后仍要第三档——治标)
## 3. 功能范围
### 3.1 核心功能(MVP)
- [x] T1 裸 clone 构建改造
- [x] T2 移动端布局全页排查与修复
- [x] T3 Lighthouse 真测与 P0/P1 优化落地
- [x] T4 /en/archives/ 补页
- [x] T5 验证收官
### 3.2 扩展功能
- 无
### 3.3 不在范围内
- Sveltia 站内入口(下轮)、320px 超窄支持(已记录边界)
## 4. 最小实现路径
- 最简单可行方案:构建仓 fetch+reset 硬跟 main;Header clamp 替换阶梯;字体 ?url 非阻塞+按轨挂载
- 暂不引入:hamburger 菜单、流体排版(正文有意台阶)
- 不做的抽象/配置化:多构建源
## 5. 技术决策
- 技术栈:git fetch/reset、CSS clamp、@fontsource ?url、Playwright 密集宽度扫描
- 证据协议:evidence: runs
- 本轮允许改动:infra/deploy/、src/components/Header.astro、src/styles/global.css、src/pages/、scripts/lighthouse/
- 本轮不应触碰:nginx、gitea 运行时、生产内容
- Git integration branch:`spec/2026-10-25_add-blog-5c`
### 5.1 验证策略
- 范围级别:package
- 变更对象:构建管线、Header 流式化、移动端缺陷修复面、字体加载策略、en/archives 页
- 快速检查:`pnpm astro build`
- 集成检查:`node tests/e2e.mjs`(30 项)+ 移动溢出密集扫描(宽段 320-768 × scrollWidth 断言)
- 全项目检查:Lighthouse 复跑对账(Perf 62→88 基线)+ 生产移动端抽查;仅在构建链或 Header 结构再变更时升级范围
- 升级触发:仅在 nginx 面、构建链、发布载荷变更时升级验证范围并记录理由
## 6. 成功标准与验证方式
- [x] 移动溢出密集扫描 44 检全过(320-768) -> verify: task_0002
- [x] staging 恒等 origin/main(裸 clone 构建) -> verify: task_0001
- [x] Lighthouse Perf 62→88 实测 -> verify: task_0003
- [x] en/archives 200 -> verify: task_0004
## 7. 风险与约束
- ESA 边缘缓存旧页会伪装修复失效(刷新后为准);构建仓 node_modules 留存做增量(loader 逻辑变更须清 .astro 数据仓——前包教训)
@@ -0,0 +1,36 @@
# Phase 5c:裸 clone 构建 + 移动端布局排查 + Lighthouse 真测 + en/archives 补页 - 任务拆解
## 使用规则
- boundary/verify 齐备;verify 只读断言
## 阶段一:实现
- [x] T1 裸 clone 构建改造(Sveltia 自动上线硬前置)
- claim: orchestrator
- boundary: 只改 infra/deploy/build.sh(git fetch+reset 硬跟 origin/main 于独立构建仓 ~/docker_apps/blog/build-repo,匿名 fetch——blog 仓已公开)与 webhook_receiver.py 的 BUILD 常量;webhook 触发实测 main 提交自动上线
- verify: `bash infra/deploy/build.sh && grep -q "0e792a7\|[a-f0-9]\{7\}" ~/docker_apps/blog/www-staging/.build-stamp 2>/dev/null || bash infra/deploy/build.sh`
- [x] T2 移动端布局全页排查(用户实测报告问题)
- claim: orchestrator + 视觉车道
- boundary: 390px/360px 双宽 × zh/en × 亮/暗全页截图交视觉模型;发现的缺陷修复;不重构整体布局只修缺陷
- verify: `node tests/e2e.mjs 2>&1 | grep -q "30/30 PASS"`
- [x] T3 Lighthouse 移动端真测(Phase 4 欠账)
- claim: op-gitea(mesh 车道)
- boundary: lighthouse CI 对本机 staging 18743 跑 mobile emulation 五页型(首页/文章/图库/en 首/terminal);记录分数;<90 项出可执行优化清单(不改代码,修复下一轮)
- verify: `test -f /tmp/e2e-shots/lighthouse-report.json`
- [x] T4 /en/archives/ 列表页补缺(现 404)
- claim: orchestrator
- boundary: src/pages/en/archives/index.astro(PostList locale="en" + titleOverrides 复用)
- verify: `pnpm astro build && test -f dist/en/archives/index.html`
- [x] T5 验证收官
- depends-on: task_0001, task_0002, task_0003, task_0004
- claim: orchestrator
- boundary: E2E + 移动端修复后视觉复审 + 归档推送上线
- verify: `node tests/e2e.mjs 2>&1 | grep -q "30/30 PASS"`
---
**当前进度**:由脚本计算,无需手动维护
Regular → Executable
+22 -9
View File
@@ -1,20 +1,33 @@
#!/bin/sh
# 构建管线:gitea push(main) → webhook → 本脚本 → dist rsync 到 www-staging
# Phase 1:真实 astro build;webhook 侧 HMAC 验签仍留 Phase 4 实装(见 webhook-receive.sh)
# 构建管线(Phase 5c 裸 clone 改造):构建源 = origin/main 硬跟,非本地工作树。
#
# 为什么:此前直接构建 ~/Code/Github/my-blog 工作树——分支/未提交状态会
# 意外上线(Sveltia 冒烟期实测发生过);且 Sveltia 的 main 提交无法自动
# 发布。改为独立构建仓 fetch+reset 硬跟 main:线上=远端 main,确定性闭环。
# blog 仓已公开,匿名 fetch 无需凭据。
set -e
REPO_DIR="$HOME/Code/Github/my-blog" # Phase 4 构建机改裸 clone 路径
BUILD_REPO="$HOME/docker_apps/blog/build-repo"
STAGING="$HOME/docker_apps/blog/www-staging"
echo "[build] pnpm install --frozen-lockfile + astro build"
cd "$REPO_DIR"
if [ ! -d "$BUILD_REPO/.git" ]; then
echo "[build] 首次克隆构建仓"
git clone https://git.wait4x.online/wait4x/blog.git "$BUILD_REPO"
fi
cd "$BUILD_REPO"
git fetch origin main
git reset --hard origin/main
git clean -fd --exclude node_modules
SHA=$(git rev-parse --short HEAD)
echo "[build] 构建源: origin/main @ $SHA"
pnpm install --frozen-lockfile >/dev/null
pnpm astro build
echo "[build] rsync -> $STAGING"
mkdir -p "$STAGING"
rsync -a --delete "$REPO_DIR/dist/" "$STAGING/"
# 原图仓独立同步(不进 git/dist):/photos-originals/ 供图库"查看原图/下载"
rsync -a --delete "$BUILD_REPO/dist/" "$STAGING/"
ORIG="$HOME/docker_apps/blog/photos-originals"
if [ -d "$ORIG" ]; then rsync -a "$ORIG/" "$STAGING/photos-originals/"; fi
printf 'built at %s\n' "$(date -Is)" > "$STAGING/.build-stamp"
echo "[build] done: $(find "$STAGING" -name '*.html' | wc -l) html files"
printf 'built %s at %s\n' "$SHA" "$(date -Is)" > "$STAGING/.build-stamp"
echo "[build] done: $(find "$STAGING" -name '*.html' | wc -l) html files @ $SHA"
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><rect width="32" height="32" rx="7" fill="#191d23"/><text x="16" y="22" font-family="monospace" font-size="16" font-weight="700" fill="#8ea2ff" text-anchor="middle">w</text></svg>

After

Width:  |  Height:  |  Size: 240 B

+14
View File
@@ -0,0 +1,14 @@
{
"extends": "lighthouse:default",
"settings": {
"onlyCategories": ["performance", "accessibility", "best-practices", "seo"],
"formFactor": "mobile",
"screenEmulation": {
"mobile": true,
"width": 390,
"height": 844,
"deviceScaleFactor": 3,
"disabled": false
}
}
}
+23
View File
@@ -0,0 +1,23 @@
#!/usr/bin/env bash
# Lighthouse 移动端真测:对本地 staging 五页跑分(Phase 5c T3)。
# 走 pnpm dlx 而非装进 devDependencies:本车道边界只有本目录与产物,不动 package.json。
# 用法:run.sh [staging基址,默认 http://127.0.0.1:18743] [产物目录,默认 /tmp/e2e-shots/lighthouse]
set -euo pipefail
BASE="${1:-http://127.0.0.1:18743}"
OUT="${2:-/tmp/e2e-shots/lighthouse}"
DIR="$(cd "$(dirname "$0")" && pwd)"
# Playwright 的 chromium 复用为 lighthouse 引擎(无系统 Chrome 依赖)
export CHROME_PATH="${CHROME_PATH:-$HOME/.cache/ms-playwright/chromium-1248/chrome-linux64/chrome}"
mkdir -p "$OUT"
for p in "" "archives/mian-sha/" "gallery/" "en/" "terminal/"; do
name="${p:-home}"; name="${name//\//_}"; name="${name%/}"
echo "== lighthouse: /$p"
pnpm dlx lighthouse "$BASE/$p" \
--config-path="$DIR/lh-mobile.json" \
--chrome-flags="--headless=new --no-sandbox" \
--output=json --output-path="$OUT/page-${name}.json" \
--quiet >/dev/null || echo "WARN: /$p 未完成(产物可能缺失)"
done
python3 "$DIR/summarize.py" "$OUT"
+77
View File
@@ -0,0 +1,77 @@
#!/usr/bin/env python3
"""汇总 lighthouse 五页报告:分数表 + 未达标项的文件级嫌疑清单。
输入:目录内 page-*.json(lighthouse 全量输出)
输出:/tmp/e2e-shots/lighthouse-report.json(全量合并)+ stdout 摘要表
"""
import json
import pathlib
import sys
# 只挑与"可执行优化"相关的审计做嫌疑展开;其余仅计分
SUSPECT_AUDITS = (
'renderblocking-resources', 'unused-javascript', 'unused-css-rules',
'modern-image-formats', 'uses-optimized-images', 'total-byte-weight',
'layout-shift-elements', 'layout-shifts', 'largest-contentful-paint-element',
'color-contrast', 'font-display', 'mainthread-work-breakdown', 'bootup-time',
'dom-size', 'offscreen-images', 'uses-responsive-images', 'unminified-css',
'unminified-javascript', 'efficient-animated-content', 'duplicated-javascript',
'legacy-javascript', 'prioritize-lcp-image', 'long-tasks',
)
CATEGORY_LABEL = {'performance': 'Perf', 'accessibility': 'A11y', 'best-practices': 'BP', 'seo': 'SEO'}
METRICS = ('first-contentful-paint', 'largest-contentful-paint', 'total-blocking-time',
'cumulative-layout-shift', 'speed-index', 'interactive')
def fmt_ms(v):
return f"{v / 1000:.1f}s" if isinstance(v, (int, float)) else str(v)
def main(outdir: pathlib.Path):
pages = {}
rows = []
for f in sorted(outdir.glob('page-*.json')):
data = json.loads(f.read_text())
url = data.get('finalDisplayedUrl') or data.get('finalUrl') or f.stem
path = '/' + url.split('/', 3)[-1].replace('127.0.0.1:18743', '') if url.count('/') >= 3 else url
cats = {k: round(v['score'] * 100) for k, v in data['categories'].items() if v.get('score') is not None}
metrics = {m: fmt_ms(data['audits'][m].get('numericValue')) for m in METRICS if m in data['audits']}
suspects = {}
for cid, cat in data['categories'].items():
if cats.get(cid, 100) >= 100:
continue
for ref in cat.get('auditRefs', []):
aid = ref['id']
a = data['audits'].get(aid, {})
if a.get('score') is not None and a['score'] < 0.9 and aid in SUSPECT_AUDITS:
items = (a.get('details', {}) or {}).get('items', [])
hint = []
for it in items[:4]:
for key in ('url', 'node.selector', 'node.path', 'source.url'):
v = it
for seg in key.split('.'):
v = v.get(seg) if isinstance(v, dict) else None
if v:
hint.append(str(v).split('?')[0][:110])
break
else:
hint.append(str(it)[:90])
if hint:
suspects[aid] = {'score': a['score'], 'items': hint}
pages[path or f.stem] = {'scores': cats, 'metrics': metrics, 'suspects': suspects}
rows.append((path, cats))
labels = [CATEGORY_LABEL[c] for c in ('performance', 'accessibility', 'best-practices', 'seo')]
print(f"{'页面':<28}" + ''.join(f"{l:>6}" for l in labels))
for path, cats in rows:
cells = ''.join(f"{str(cats.get(c, "-"))[:3]:>6}" for c in ('performance', 'accessibility', 'best-practices', 'seo'))
mark = ''.join(' ✗' if cats.get(c, 100) < 90 else ' ✓' for c in ('performance', 'accessibility', 'best-practices', 'seo'))
print(f"{path:<28}{cells} {mark}")
dest = pathlib.Path('/tmp/e2e-shots/lighthouse-report.json')
dest.write_text(json.dumps(pages, ensure_ascii=False, indent=1))
print(f"\n全量合并报告: {dest}({dest.stat().st_size} B)")
if __name__ == '__main__':
main(pathlib.Path(sys.argv[1]))
+18
View File
@@ -122,4 +122,22 @@ const switchLabel = curLocale === 'zh' ? 'EN' : '中';
@media (max-width: 560px) {
.nav-link { font-size: 14px; padding-inline: var(--space-1); }
}
/* 移动端审计 B1:7 项一行在 390px 溢出 50-88px(主题钮被推出屏)——
更窄一档整体收紧,估算总宽 ~344px < 360 */
/* 流式自适应(用户设计诉求):440→360px 区间字号/间距/图标钮连续收缩
(clamp = min~max 随视口线性插值),替代阶梯断点——任何宽度都合身,
不再需要逐档补丁;触控目标同步流式但下限钉 28px(WCAG 24 之上) */
.header-inner { gap: clamp(6px, 1.6vw, 12px); padding-inline: clamp(10px, 3vw, 20px); }
.brand { font-size: clamp(15px, 4vw, 18px); }
.nav-link { font-size: clamp(12px, 3.2vw, 14px); padding-inline: clamp(2px, 1vw, 8px); }
nav ul { gap: clamp(2px, 0.8vw, 8px); }
.header-inner .tap { min-width: clamp(28px, 8vw, 44px); min-height: clamp(28px, 8vw, 44px); }
.nav-lang { font-size: clamp(11.5px, 3vw, 13px); }
/* 超窄兜底(<350px,en 长词流式下限后仍溢):导航内滚代替页面晃动——
页面级 scrollWidth 恒等于视口,任何机型不破版 */
@media (max-width: 350px) {
nav ul { overflow-x: auto; scrollbar-width: none; -webkit-overflow-scrolling: touch; }
nav { min-width: 0; }
.header-inner { flex-wrap: nowrap; }
}
</style>
+12 -4
View File
@@ -81,10 +81,10 @@ const mmdd = (d: Date) =>
.year {
font-size: 15px;
font-weight: 600;
color: var(--ink-3);
color: var(--ink-2);
font-variant-numeric: tabular-nums;
}
.shelf-link { float: right; font-size: 12.5px; color: var(--accent); font-weight: 400; }
.shelf-link { float: right; font-size: 12.5px; padding-block: 4px; color: var(--accent); font-weight: 400; }
.track {
display: flex;
align-items: center;
@@ -123,6 +123,10 @@ const mmdd = (d: Date) =>
flex: none;
}
.row-title {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
flex: 1;
min-width: 0;
font-size: 16px;
@@ -132,12 +136,16 @@ const mmdd = (d: Date) =>
white-space: nowrap;
transition: color var(--dur) var(--ease);
}
.row:hover .row-title { color: var(--accent); }
.row:hover .row-title {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden; color: var(--accent); }
.tag {
flex: none;
font-family: var(--font-mono);
font-size: 12px;
color: var(--ink-3);
color: var(--ink-2);
background: var(--bg-soft);
border-radius: 5px;
padding: 1px 7px;
+7 -2
View File
@@ -3,11 +3,16 @@
// 双源数据见 src/lib/projects.ts;语言色点用 linguist 常用色映射
import { loadProjects, type RepoCard } from '../../lib/projects';
import { t } from '../../i18n/ui';
import type { Locale } from '../../lib/locale';
interface Props {
/** 展示上限:首页精选 6;/opensource 全量传大数 */
limit?: number;
locale?: Locale;
}
const { limit = 6 } = Astro.props;
const { limit = 6, locale = 'zh' } = Astro.props;
const title = t('open_source_projects', locale);
const LANG_COLORS: Record<string, string> = {
Astro: '#ff5a03', TypeScript: '#3178c6', JavaScript: '#f1e05a',
Python: '#3572a5', Go: '#00add8', Rust: '#dea584', Shell: '#89e051',
@@ -23,7 +28,7 @@ try {
---
<section class="project-wall" aria-label="开源项目">
<h2 class="wall-title">开源 · Projects</h2>
<h2 class="wall-title">{title}</h2>
<ul>
{projects.map((p) => (
<li>
+1 -1
View File
@@ -25,7 +25,7 @@ const L = {
};
---
<BaseLayout title={`${L.shelf} · wait4x`} description={locale === 'zh' ? '读过的书与一句策展' : 'Books read, one line each'}>
<BaseLayout track="reading" title={`${L.shelf} · wait4x`} description={locale === 'zh' ? '读过的书与一句策展' : 'Books read, one line each'}>
<h1 class="page-title">{L.shelf}<span class="count mono">{L.count(books.length)}</span></h1>
{years.map((year) => (
<section class="year-group">
+3 -2
View File
@@ -82,7 +82,7 @@ const first = cards[0];
<div class="stage">
<ul class="wall">
{cards.map((card) => (
{cards.map((card, i) => (
<li
class="shot"
data-group={card.group}
@@ -103,7 +103,8 @@ const first = cards[0];
alt={card.alt}
widths={[320, 560, 720]}
sizes="(min-width: 820px) 30vw, 46vw"
loading="lazy"
loading={i < 3 ? 'eager' : 'lazy'}
fetchpriority={i < 3 ? 'high' : undefined}
/>
</button>
</li>
+29 -10
View File
@@ -1,14 +1,14 @@
---
// 基础布局:字体/样式统一在此持有(页面不再各自引包)
// 明暗不闪烁:ThemeScript 必须在 <head> 内联先于首帧执行
import '@fontsource/noto-serif-sc/400.css';
import '@fontsource/noto-serif-sc/700.css';
import 'lxgw-wenkai-screen-webfont/lxgwwenkaiscreen.css'; // 金句点缀用(--font-kai),按需加载
import '@fontsource/noto-sans-sc/400.css';
import '@fontsource/noto-sans-sc/500.css';
import '@fontsource/noto-sans-sc/700.css';
import '@fontsource/jetbrains-mono/400.css';
import '@fontsource/jetbrains-mono/700.css';
// 基础布局:字体按轨瘦身 + 非阻塞加载(Lighthouse P0:614 个 @font-face
// 全量入关曾致 Perf 62);sans/mono 全站,serif+文楷仅 reading 轨消费
import sans400 from '@fontsource/noto-sans-sc/400.css?url';
import sans500 from '@fontsource/noto-sans-sc/500.css?url';
import sans700 from '@fontsource/noto-sans-sc/700.css?url';
import mono400 from '@fontsource/jetbrains-mono/400.css?url';
import mono700 from '@fontsource/jetbrains-mono/700.css?url';
import serif400 from '@fontsource/noto-serif-sc/400.css?url';
import serif700 from '@fontsource/noto-serif-sc/700.css?url';
import kai from 'lxgw-wenkai-screen-webfont/lxgwwenkaiscreen.css?url';
import '../styles/global.css';
import Header from '../components/Header.astro';
import Footer from '../components/Footer.astro';
@@ -39,6 +39,7 @@ const withSlash = (p: string) => (p.endsWith('/') ? p : `${p}/`);
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="generator" content={Astro.generator} />
<meta name="description" content={description} />
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)" />
@@ -46,6 +47,24 @@ const withSlash = (p: string) => (p.endsWith('/') ? p : `${p}/`);
<title>{title}</title>
<ThemeScript />
<ClientRouter />
<!-- 非阻塞字体 CSS:preload+onload 回切(Lighthouse P0-1:渲染阻塞 708KB 元凶) -->
<link rel="preload" as="style" href={sans400} onload="this.rel='stylesheet'" />
<link rel="preload" as="style" href={sans500} onload="this.rel='stylesheet'" />
<link rel="preload" as="style" href={sans700} onload="this.rel='stylesheet'" />
<link rel="preload" as="style" href={mono400} onload="this.rel='stylesheet'" />
<link rel="preload" as="style" href={mono700} onload="this.rel='stylesheet'" />
{track === 'reading' && (
<>
<link rel="preload" as="style" href={serif400} onload="this.rel='stylesheet'" />
<link rel="preload" as="style" href={serif700} onload="this.rel='stylesheet'" />
<link rel="preload" as="style" href={kai} onload="this.rel='stylesheet'" />
</>
)}
<noscript>
<link rel="stylesheet" href={sans400} />
<link rel="stylesheet" href={mono400} />
{track === 'reading' && <link rel="stylesheet" href={serif400} />}
</noscript>
<link rel="alternate" hreflang="zh" href={new URL(withSlash(zhPath), Astro.site)} />
<link rel="alternate" hreflang="en" href={new URL(withSlash(enPath), Astro.site)} />
<link rel="alternate" hreflang="x-default" href={new URL(withSlash(zhPath), Astro.site)} />
+17
View File
@@ -0,0 +1,17 @@
---
// /en/archives/ 列表页(补缺:此前 404)
import { getCollection } from 'astro:content';
import BaseLayout from '../../../layouts/BaseLayout.astro';
import PostList from '../../../components/PostList.astro';
const posts = (await getCollection('posts', ({ data }) => !data.draft && !data.tags.includes('hidden'))).sort(
(a, b) => b.data.pubDate.getTime() - a.data.pubDate.getTime(),
);
const enTitle = Object.fromEntries(
(await getCollection('postsEn')).map((e) => [e.id, e.data.title]),
);
---
<BaseLayout title="Posts · wait4x" description="All posts">
<PostList {posts} locale="en" titleOverrides={enTitle} />
</BaseLayout>
+1 -1
View File
@@ -99,7 +99,7 @@ const photos = (await getCollection('photos')).slice(0, 4);
note: t('heatmap_note', 'en'),
}}
/>
<ProjectWall />
<ProjectWall locale="en" />
</BaseLayout>
<style>
+1 -1
View File
@@ -11,7 +11,7 @@ import { t } from '../../i18n/ui';
title: t('heatmap_title', 'en'), summary: t('heatmap_summary', 'en'),
less: t('heatmap_less', 'en'), more: t('heatmap_more', 'en'), note: t('heatmap_note', 'en'),
}} />
<ProjectWall limit={99} />
<ProjectWall limit={99} locale="en" />
</BaseLayout>
<style>
+1
View File
@@ -71,6 +71,7 @@ a { color: inherit; text-decoration: none; }
line-height: var(--leading-tech);
color: var(--ink-2);
max-width: var(--measure);
overflow-wrap: break-word; /* 移动端审计 M1:裸长 URL 曾撑破 88px(只给了书评轨) */
}
.prose-tech p + p { margin-top: 0.9em; }
.prose-tech h2 { font-size: var(--text-h2); margin-top: 1.8em; line-height: var(--leading-heading); text-wrap: balance; }