diff --git a/.spec/specs/archive/2026-10-26_blog-5f/completion-summary.md b/.spec/specs/archive/2026-10-26_blog-5f/completion-summary.md new file mode 100644 index 0000000..934dd9e --- /dev/null +++ b/.spec/specs/archive/2026-10-26_blog-5f/completion-summary.md @@ -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 +} +``` diff --git a/.spec/specs/archive/2026-10-26_blog-5f/runs.jsonl b/.spec/specs/archive/2026-10-26_blog-5f/runs.jsonl new file mode 100644 index 0000000..be30b84 --- /dev/null +++ b/.spec/specs/archive/2026-10-26_blog-5f/runs.jsonl @@ -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"} diff --git a/.spec/specs/archive/2026-10-26_blog-5f/spec.md b/.spec/specs/archive/2026-10-26_blog-5f/spec.md new file mode 100644 index 0000000..129abbf --- /dev/null +++ b/.spec/specs/archive/2026-10-26_blog-5f/spec.md @@ -0,0 +1,87 @@ +# Phase 5f:SEO 解封与功能优化批 + 美化首批 - 项目范围 + + + +## 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 动 diff --git a/.spec/specs/archive/2026-10-26_blog-5f/tasks.md b/.spec/specs/archive/2026-10-26_blog-5f/tasks.md new file mode 100644 index 0000000..dbadca8 --- /dev/null +++ b/.spec/specs/archive/2026-10-26_blog-5f/tasks.md @@ -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` diff --git a/docs/上线检查单.md b/docs/上线检查单.md index d232dfb..7d4332a 100644 --- a/docs/上线检查单.md +++ b/docs/上线检查单.md @@ -36,3 +36,28 @@ sudo bash infra/deploy/rollback.sh --exec # 秒级回 Halo - 新文章工作流:本地或 gitea web 写 md → push main → webhook 自动构建发布 - 新照片:上传 content/photos/ 后跑 `python3 scripts/photos_ingest.py` 再 commit - 翻译:新文章 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 复测随后入档。 diff --git a/infra/deploy/build.sh b/infra/deploy/build.sh index 33c07d3..0d76e43 100755 --- a/infra/deploy/build.sh +++ b/infra/deploy/build.sh @@ -31,6 +31,9 @@ pnpm astro build [ -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 脚本未就绪,跳过" +# 内容 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" mkdir -p "$STAGING" rsync -a --delete "$BUILD_REPO/dist/" "$STAGING/" @@ -38,3 +41,9 @@ ORIG="$HOME/docker_apps/blog/photos-originals" if [ -d "$ORIG" ]; then rsync -a "$ORIG/" "$STAGING/photos-originals/"; fi printf 'built %s at %s\n' "$SHA" "$(date -Is)" > "$STAGING/.build-stamp" 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 diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000..7cc7bd7 --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,9 @@ +# wait4x.online —— 搜索引擎指引(Phase 5f T1) +# 背景:上线期 nginx 共享 snippet 误注全站 noindex,本文件随 SEO 解封三件套落地 +# (docs/上线检查单.md「SEO 解封操作单」;robots 曾长期 404) +User-agent: * +Allow: / +Disallow: /admin/ +Disallow: /draft/ + +Sitemap: https://wait4x.online/sitemap-index.xml diff --git a/scripts/build-perf/content-lint.mjs b/scripts/build-perf/content-lint.mjs new file mode 100644 index 0000000..3374766 --- /dev/null +++ b/scripts/build-perf/content-lint.mjs @@ -0,0 +1,80 @@ +#!/usr/bin/env node +// 构建期内容 lint(T3,零依赖):扫 dist 产物三检——站内断链 / 图片存在 / zh↔en 配对 +// 用法:node scripts/build-perf/content-lint.mjs [dist目录=dist] +// 逃生门:CONTENT_LINT=skip 整体跳过;CONTENT_LINT=warn 只警告不炸构建(首版兼容期) +// 口径:断链/缺图 = FAIL(exit 1);配对缺失 = WARN(en 缺页可能是有意的,人工裁决) +import { readdirSync, readFileSync, existsSync, statSync } from 'node:fs'; +import { join, resolve as pathResolve } from 'node:path'; + +const DIST = pathResolve(process.argv[2] ?? 'dist'); +const MODE = process.env.CONTENT_LINT ?? 'fail'; +if (MODE === 'skip') { + console.log('[content-lint] CONTENT_LINT=skip,整体跳过'); + process.exit(0); +} + +const SKIP_PREFIX = ['/admin/', '/photos-originals/']; // admin=CMS 路由;photos-originals=部署期 rsync 叠加层(不在 dist,另有部署链保障) +const SKIP_FILE = new Set(['/404.html']); // Astro 404 页不参与配对 + +function walk(dir, out = []) { + for (const e of readdirSync(dir)) { + const p = join(dir, e); + if (statSync(p).isDirectory()) walk(p, out); + else if (e.endsWith('.html')) out.push(p); + } + return out; +} + +/** /x/ → dist/x/index.html;/a.svg → dist/a.svg;找不到即 null */ +function targetExists(url) { + if (SKIP_PREFIX.some((s) => url.startsWith(s))) return true; + const p = url.split('#')[0].split('?')[0]; + if (!p || p === '/') return existsSync(join(DIST, 'index.html')); + if (p.endsWith('/')) return existsSync(join(DIST, p, 'index.html')); + return existsSync(join(DIST, p)) || existsSync(join(DIST, `${p}.html`)) || existsSync(join(DIST, p, 'index.html')); +} + +const files = walk(DIST); +const fails = []; +const warns = []; + +for (const f of files) { + const html = readFileSync(f, 'utf8'); + const rel = f.slice(DIST.length) || '/'; + + // 站内 :仅根相对路径;#锚点/mailto/tel/外部/data: 全跳过 + for (const m of html.matchAll(/]*href="([^"]*)"/g)) { + const href = m[1]; + if (!href.startsWith('/') || href.startsWith('//')) continue; + if (!targetExists(href)) fails.push(`断链 ${rel} → ${href}`); + } + + // 与 srcset:产物必须存在(构建期副本检查,防部署后 404 图) + for (const m of html.matchAll(/]*>/g)) { + const tag = m[0]; + const src = /(?:\ssrc="([^"]*)")/.exec(tag)?.[1]; + const srcset = /(?:\ssrcset="([^"]*)")/.exec(tag)?.[1]; + for (const u of [src, ...(srcset ?? '').split(',').map((s) => s.trim().split(/\s+/)[0])]) { + if (!u || !u.startsWith('/') || u.startsWith('//')) continue; + if (!targetExists(u)) fails.push(`缺图 ${rel} → ${u}`); + } + } + + // zh↔en 配对:dist/en/x/index.html ↔ dist/x/index.html,双向 WARN + if (!SKIP_FILE.has(rel) && !SKIP_PREFIX.some((s) => rel.startsWith(s))) { + const other = rel.startsWith('/en/') ? rel.slice(3) : `/en${rel === '/index.html' ? '/index.html' : rel}`; + if (!existsSync(join(DIST, other))) warns.push(`配对缺 ${rel} ↔ ${other}`); + } +} + +for (const w of warns) console.warn(`[content-lint] WARN ${w}`); +if (fails.length) { + for (const x of fails) console.error(`[content-lint] FAIL ${x}`); + if (MODE === 'warn') { + console.warn(`[content-lint] WARN 模式:${fails.length} 个失败降级为警告`); + process.exit(0); + } + console.error(`[content-lint] ${fails.length} 失败 / ${warns.length} 警告`); + process.exit(1); +} +console.log(`[content-lint] 通过:${files.length} 页扫描,0 断链 0 缺图,${warns.length} 配对警告`); diff --git a/scripts/build-perf/prod-smoke.sh b/scripts/build-perf/prod-smoke.sh new file mode 100755 index 0000000..72f56fc --- /dev/null +++ b/scripts/build-perf/prod-smoke.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +# 生产冒烟(T3):部署后三断言——robots 带 Sitemap / sitemap-index 可达 / 首页无 x-robots-tag +# 用法:./prod-smoke.sh [BASE_URL](默认 https://wait4x.online;构建机对 staging 传 http://127.0.0.1:18743) +# 任一断言失败 exit 1;robots 缺失常见于 SEO 解封前(T1 未落),会明确报 FAIL 而非静默 +set -u +BASE="${1:-${SMOKE_BASE:-https://wait4x.online}}" +fail=0 + +robots="$(curl -fsS --max-time 15 "${BASE}/robots.txt" 2>/dev/null)" \ + && grep -q 'Sitemap:' <<<"$robots" \ + && echo "[smoke] robots.txt 200 含 Sitemap ✓" \ + || { echo "[smoke] FAIL robots.txt 缺失/无 Sitemap 行:${BASE}/robots.txt"; fail=1; } + +curl -fsS --max-time 15 -o /dev/null "${BASE}/sitemap-index.xml" \ + && echo "[smoke] sitemap-index 200 ✓" \ + || { echo "[smoke] FAIL sitemap-index 不可达:${BASE}/sitemap-index.xml"; fail=1; } + +# x-robots-tag 出现即 FAIL(noindex 头会废掉整站收录,SEO 解封的反面) +if curl -fsS --max-time 15 -I "${BASE}/" | grep -qi 'x-robots-tag'; then + echo "[smoke] FAIL 首页响应头含 x-robots-tag"; fail=1 +else + echo "[smoke] 首页无 x-robots-tag ✓" +fi + +exit $fail diff --git a/src/components/CommandPalette.astro b/src/components/CommandPalette.astro index 889e6a0..4e3a91a 100644 --- a/src/components/CommandPalette.astro +++ b/src/components/CommandPalette.astro @@ -39,6 +39,10 @@ const navItems = [ {t('cmd_group_actions', locale)} + + {t('cmd_reader', locale)} + r + {t('cmd_toggle_theme', locale)} t @@ -134,6 +138,12 @@ const navItems = [ } switch (item.dataset.cmd) { // 复用 Header 主题钮的现成切换(同一 localStorage key),不另起第二套实现 + case 'reader': { + const root = document.documentElement; + root.classList.toggle('reader'); + localStorage.setItem('reader', root.classList.contains('reader') ? '1' : '0'); + break; + } case 'theme': { const btn = document.querySelector('[data-theme-toggle]') as HTMLElement | null; if (btn) { diff --git a/src/components/ThemeScript.astro b/src/components/ThemeScript.astro index 6d2d56f..7493ac2 100644 --- a/src/components/ThemeScript.astro +++ b/src/components/ThemeScript.astro @@ -13,5 +13,7 @@ ? saved : (matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'); document.documentElement.dataset.theme = theme; - })(); + })(); + // reader 模式预绘(5f T4):与主题同点位读取,避免可交互前闪装饰 + if (localStorage.getItem('reader') === '1') document.documentElement.classList.add('reader'); diff --git a/src/components/geek/Keyboard.astro b/src/components/geek/Keyboard.astro index 9b92194..f0d8b91 100644 --- a/src/components/geek/Keyboard.astro +++ b/src/components/geek/Keyboard.astro @@ -65,6 +65,13 @@ const routes: Record = { if (inField() || e.metaKey || e.ctrlKey || e.altKey) return; if (e.key === 't') { toggleTheme(); return; } + if (e.key === 'r') { + // reader 纯文本模式(5f T4):与命令面板/持久化同源 + const root = document.documentElement; + root.classList.toggle('reader'); + localStorage.setItem('reader', root.classList.contains('reader') ? '1' : '0'); + return; + } if (e.key === '?') { (document.getElementById('kbd-help') as HTMLDialogElement | null)?.showModal(); return; diff --git a/src/components/pages/GalleryPage.astro b/src/components/pages/GalleryPage.astro index 076381c..ed6940c 100644 --- a/src/components/pages/GalleryPage.astro +++ b/src/components/pages/GalleryPage.astro @@ -429,3 +429,12 @@ const first = cards[0]; .lb-meta { order: 3; flex-basis: 100%; } } + + diff --git a/src/i18n/ui.ts b/src/i18n/ui.ts index 909adb2..5f9ba2a 100644 --- a/src/i18n/ui.ts +++ b/src/i18n/ui.ts @@ -10,6 +10,7 @@ const dict = { nav_posts: { zh: '文章', en: 'Posts' }, nav_gallery: { zh: '图库', en: 'Gallery' }, nav_about: { zh: '关于', en: 'About' }, + cmd_reader: { zh: '阅读模式(纯文本)', en: 'Reader mode (plain text)' }, nav_opensource: { zh: '开源', en: 'Projects' }, // en 用短词:390-430px 尾部主题钮免内滚(用户拍板) nav_books: { zh: '书架', en: 'Books' }, kbd_gs_books: { zh: '书架', en: 'Books' }, diff --git a/src/layouts/BaseLayout.astro b/src/layouts/BaseLayout.astro index 0f3c529..b42c977 100644 --- a/src/layouts/BaseLayout.astro +++ b/src/layouts/BaseLayout.astro @@ -30,6 +30,8 @@ const rawPath = Astro.url.pathname.replace(/\/index\.html$/, ''); const zhPath = localeOf(rawPath) === 'en' ? localePath(rawPath, 'zh') : rawPath; const enPath = localePath(rawPath, 'en'); const withSlash = (p: string) => (p.endsWith('/') ? p : `${p}/`); +// 自指 canonical(T2):SEO 权重收敛到当前页自身,与 hreflang 组合覆盖双轨 +const canonicalUrl = new URL(withSlash(rawPath), Astro.site).href; --- @@ -87,6 +89,7 @@ const withSlash = (p: string) => (p.endsWith('/') ? p : `${p}/`); addEventListener('load', () => navigator.serviceWorker.register('/sw.js')); } + diff --git a/src/lib/locale.ts b/src/lib/locale.ts index 8d4d712..fa3ee65 100644 --- a/src/lib/locale.ts +++ b/src/lib/locale.ts @@ -7,9 +7,12 @@ export type Locale = 'zh' | 'en'; export const localeOf = (pathname: string): Locale => pathname === '/en' || pathname.startsWith('/en/') ? 'en' : 'zh'; -/** 当前路径在另一语言下的对应路径(zh → /en/...,en → 去 /en 前缀)。 */ +/** 当前路径在另一语言下的对应路径(zh → /en/...,en → 去 /en 前缀)。 + * 尾斜杠归一:返回值一律带尾斜杠(与全站/sitemap 口径一致)。 + * 先归一 zh 再拼前缀——en→en 场景若直接 /en+clean 会产出 /en/en/(T2 病根) */ export function localePath(pathname: string, to: Locale): string { const clean = pathname.replace(/\/index\.html$/, '').replace(/\/$/, '') || '/'; - if (to === 'en') return clean === '/' ? '/en/' : `/en${clean}`; - return clean.replace(/^\/en(?=\/|$)/, '') || '/'; + const zh = clean.replace(/^\/en(?=\/|$)/, '') || '/'; + const slash = (p: string) => (p === '/' ? '/' : `${p}/`); + return to === 'en' ? slash(zh === '/' ? '/en' : `/en${zh}`) : slash(zh); } diff --git a/src/pages/archives/[...slug].astro b/src/pages/archives/[...slug].astro index 5cd0ac0..0ba64cd 100644 --- a/src/pages/archives/[...slug].astro +++ b/src/pages/archives/[...slug].astro @@ -133,6 +133,24 @@ const toc = headings.filter((h) => h.depth === 2 || h.depth === 3); addEventListener('scroll', paint, { passive: true }); document.addEventListener('astro:page-load', paint); } + + // 代码块窗条标记(P 卡 §6-3):纯 CSS 无法按内容行数条件挂伪元素, + // 此处数行加类;语言标签取 shiki 的 language-* 类名写入 data-lang 供 CSS attr() 渲染 + const markCodeWins = () => { + document.querySelectorAll('.prose-tech pre:not(.code-win)').forEach((pre) => { + const code = pre.querySelector('code'); + if (!code) return; + const lines = (code.textContent ?? '').split('\n').length; + if (lines >= 4) { + // astro shiki 语言落 pre[data-language](code 类名为空) + const lang = pre.dataset.language ?? /language-([\w-]+)/.exec(code.className)?.[1] ?? 'text'; + pre.dataset.lang = lang; + pre.classList.add('code-win'); + } + }); + }; + document.addEventListener('astro:page-load', markCodeWins); + markCodeWins(); diff --git a/src/pages/en/archives/[...slug].astro b/src/pages/en/archives/[...slug].astro index 173c4c7..c5ad37a 100644 --- a/src/pages/en/archives/[...slug].astro +++ b/src/pages/en/archives/[...slug].astro @@ -78,4 +78,64 @@ const minutes = Math.max(1, Math.ceil(words / 220)); .pn { display: flex; flex-direction: column; gap: var(--space-2); margin-top: var(--space-5); } .pn-item { font-size: 14px; color: var(--ink-2); transition: color var(--dur) var(--ease); } .pn-item:hover { color: var(--accent); } + + /* ---- T5 code window bar (§6-3): ≥4-line blocks only ---- */ + .prose-tech :global(pre.code-win) { + position: relative; + padding-top: calc(var(--space-3) + 30px); + } + .prose-tech :global(pre.code-win::before) { + content: attr(data-lang); + position: absolute; + inset: 0 0 auto 0; + height: calc(var(--space-3) + 30px - 1px); + display: flex; + align-items: flex-end; + padding: 0 14px 8px 66px; + background: #161b22; + border-bottom: 1px solid rgba(255, 255, 255, 0.06); + border-radius: 8px 8px 0 0; + font-family: var(--font-mono); + font-size: 11px; + letter-spacing: 0.04em; + color: #6e7681; + text-transform: lowercase; + } + .prose-tech :global(pre.code-win::after) { + content: ''; + position: absolute; + top: 10px; + left: 14px; + width: 10px; + height: 10px; + border-radius: 50%; + background: #30363d; + box-shadow: 18px 0 0 #30363d, 36px 0 0 #30363d; + } + + /* ---- T6 reading paper card (§6-2) ---- */ + .prose-reading { + border: 1px solid var(--paper-line); + border-radius: 12px; + padding: var(--space-5) clamp(20px, 4vw, 32px); + } + + diff --git a/src/pages/index.astro b/src/pages/index.astro index a1167ca..b506092 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -19,7 +19,8 @@ const heartbeat = await fetchHeartbeat(); - + 写作者 · 读者 · 快门按压者,拥抱开源。 +
{t('cmd_group_actions', locale)}
写作者 · 读者 · 快门按压者,拥抱开源。