Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ef2bd97267 | ||
|
|
5d187e1240 | ||
|
|
1d579c2281 | ||
|
|
783d74966e | ||
|
|
5c0e6ed900 | ||
|
|
a555a3e450 | ||
|
|
d37fe581c0 | ||
|
|
9b11070dd4 | ||
|
|
a98b2e83da | ||
|
|
3ec7e48727 | ||
|
|
eb5e3f851e | ||
|
|
35cb6e764e | ||
|
|
3c15dd47a5 | ||
|
|
1942d5b7f0 | ||
|
|
cf331ccf3d | ||
|
|
eead28f3a0 | ||
|
|
44cd62e9a2 | ||
|
|
97a56f3c89 | ||
|
|
dac34edd9a | ||
|
|
1737bf027c | ||
|
|
d4ddc08fbd | ||
|
|
b99e6e744b | ||
|
|
a3d6998de5 | ||
|
|
ac1eab3cf1 | ||
|
|
0d40b00f5a | ||
|
|
326b9bbd79 | ||
|
|
59895aca2b | ||
|
|
c5889f960b | ||
|
|
a3ddbbefdd | ||
|
|
e65d44df29 | ||
|
|
609ce1eb2e | ||
|
|
f6da3b3c32 | ||
|
|
160af791b5 | ||
|
|
0ef0fcd394 | ||
|
|
b72ecccc80 | ||
|
|
9a3a196eea | ||
|
|
4153c6d33f | ||
|
|
cb44d0b2d2 | ||
|
|
b41b4ba8a5 | ||
|
|
129b23cd2b | ||
|
|
6e343c80dc | ||
|
|
f73a1b3e63 | ||
|
|
19b765e5b1 | ||
|
|
6b501fa688 | ||
|
|
f581f5bc4f | ||
|
|
a2b5755680 | ||
|
|
7401b512e6 | ||
|
|
e5f2d0089e | ||
|
|
3151d20c66 | ||
|
|
49acb4cbd1 | ||
|
|
b3c3bcf49e | ||
|
|
34a78dacb9 | ||
|
|
d6dfe39db3 | ||
|
|
d9721cdbd8 | ||
|
|
1a486a20d2 | ||
|
|
58baad5a7b | ||
|
|
6b2e4d8666 | ||
|
|
7e0167a8bb | ||
|
|
545c91a0ff | ||
|
|
7149541a92 | ||
|
|
a868bf94fd | ||
|
|
a46430b088 | ||
|
|
1ce9cd65ea | ||
|
|
6e4d6c2f88 | ||
|
|
6abca9f412 | ||
|
|
c9fb46f5a9 | ||
|
|
edc34a8a8c | ||
|
|
83e0b863fc | ||
|
|
9df86cb05b | ||
|
|
d096d45c58 | ||
|
|
073e124344 | ||
|
|
d6e08a29f9 | ||
|
|
18ea5b17b8 | ||
|
|
d119d6b555 | ||
|
|
31fb52cb06 | ||
|
|
6061c16396 | ||
|
|
cfe508ff14 | ||
|
|
b8c5e1c86e | ||
|
|
907f64472a | ||
|
|
6293fe25d5 | ||
|
|
8d82aab213 | ||
|
|
029633f00f | ||
|
|
0ff4ede91f | ||
|
|
23303bd39a | ||
|
|
31eb44caee | ||
|
|
5c015c6577 | ||
|
|
138d3d976b | ||
|
|
d30b5decbb | ||
|
|
2526b98323 | ||
|
|
36643f5cf0 | ||
|
|
52188d5b12 | ||
|
|
fef2de1d37 | ||
|
|
bdaf02f9ca | ||
|
|
c7d1e7b975 | ||
|
|
550f6d15b4 | ||
|
|
d4fe273812 | ||
|
|
0b0307b12f | ||
|
|
3e377adc7f | ||
|
|
63c9c8ad4a | ||
|
|
8e74bead62 | ||
|
|
5bf0c327bf | ||
|
|
6538311387 | ||
|
|
cdd9c61641 | ||
|
|
e1fc8bd64c | ||
|
|
cc12e016ab | ||
|
|
ed473e996a | ||
|
|
c772236d1a | ||
|
|
a89c443c25 | ||
|
|
23b79a2473 | ||
|
|
8274180f81 | ||
|
|
3e09a362cc | ||
|
|
8debd99e61 | ||
|
|
be87917443 | ||
|
|
aaa9cd9274 |
No files matched your search
@@ -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"}
|
||||||
@@ -34,13 +34,13 @@
|
|||||||
## 3. 功能范围
|
## 3. 功能范围
|
||||||
|
|
||||||
### 3.1 核心功能(MVP)
|
### 3.1 核心功能(MVP)
|
||||||
- [ ] T1 blocks 骨架:schema(discriminatedUnion)+ page-blocks.ts + BlockRenderer + prose/tracks/divider 三块 + 后台 types 声明
|
- [x] T1 blocks 骨架:schema(discriminatedUnion)+ page-blocks.ts + BlockRenderer + prose/tracks/divider 三块 + 后台 types 声明
|
||||||
- [ ] T2 块型扩充:cards/timeline/quote/figure/linklist/cta + 图标键白名单 + 图片 image() 管线
|
- [x] T2 块型扩充:cards/timeline/quote/figure/linklist/cta + 图标键白名单 + 图片 image() 管线
|
||||||
- [ ] T3 样式即数据:theme.yaml + theme.ts + BaseLayout 内联变量 + 对比度闸门脚本
|
- [x] T3 样式即数据:theme.yaml + theme.ts + BaseLayout 内联变量 + 对比度闸门脚本
|
||||||
- [ ] T4 首页细化与去重:抽 HomePage.astro 消 zh/en 双份 + 字段细化(projectsLimit/announcement 等)
|
- [x] T4 首页细化与去重:抽 HomePage.astro 消 zh/en 双份 + 字段细化(projectsLimit/announcement 等)
|
||||||
- [ ] T5 开源页收尾:README 缓存策略/超时/回落矩阵 + fallback JSON 回填 + include 未命中 WARN
|
- [x] T5 开源页收尾:README 缓存策略/超时/回落矩阵 + fallback JSON 回填 + include 未命中 WARN
|
||||||
- [ ] T6 文档与 hint:docs/管理台.md(块编辑用法)+ 写作指南扩充 + admin 全字段中文 hint
|
- [x] T6 文档与 hint:docs/管理台.md(块编辑用法)+ 写作指南扩充 + admin 全字段中文 hint
|
||||||
- [ ] T7 字体子集化工具:按页用字裁剪艺术字体为小切片 + build.sh 守卫挂载 + 实测对比
|
- [x] T7 字体子集化工具:按页用字裁剪艺术字体为小切片 + build.sh 守卫挂载 + 实测对比
|
||||||
|
|
||||||
### 3.2 扩展功能
|
### 3.2 扩展功能
|
||||||
- 无
|
- 无
|
||||||
@@ -69,13 +69,13 @@
|
|||||||
- 升级触发:若 theme 变量波及全局排版(字体字号全站变化)或 blocks 改动触及首页/文章页结构,升级为全站视觉复核与 E2E 全量并记录理由
|
- 升级触发:若 theme 变量波及全局排版(字体字号全站变化)或 blocks 改动触及首页/文章页结构,升级为全站视觉复核与 E2E 全量并记录理由
|
||||||
|
|
||||||
## 6. 成功标准与验证方式
|
## 6. 成功标准与验证方式
|
||||||
- [ ] 关于页整页由 yaml blocks 驱动,增删排序生效 -> verify: task_0001
|
- [x] 关于页整页由 yaml blocks 驱动,增删排序生效 -> verify: task_0001
|
||||||
- [ ] 九类块全部可渲染且旧内容零回归(三级回落) -> verify: task_0002
|
- [x] 九类块全部可渲染且旧内容零回归(三级回落) -> verify: task_0002
|
||||||
- [ ] theme.yaml 改值 → 产物 CSS 变量变化,对比度闸门拦截不达标 -> verify: task_0003
|
- [x] theme.yaml 改值 → 产物 CSS 变量变化,对比度闸门拦截不达标 -> verify: task_0003
|
||||||
- [ ] 首页 zh/en 由同一组件渲染且字段可配 -> verify: task_0004
|
- [x] 首页 zh/en 由同一组件渲染且字段可配 -> verify: task_0004
|
||||||
- [ ] 开源页 README 失败矩阵与 fallback 告警可用 -> verify: task_0005
|
- [x] 开源页 README 失败矩阵与 fallback 告警可用 -> verify: task_0005
|
||||||
- [ ] 后台文档与 hint 完整 -> verify: task_0006
|
- [x] 后台文档与 hint 完整 -> verify: task_0006
|
||||||
- [ ] 字体子集化工具把指定字体单页增量压到 ≤150KB -> verify: task_0007
|
- [x] 字体子集化工具把指定字体单页增量压到 ≤150KB -> verify: task_0007
|
||||||
|
|
||||||
## 7. 风险与约束
|
## 7. 风险与约束
|
||||||
- 灰阶体系与双语红线不变;彩色仅正文强调
|
- 灰阶体系与双语红线不变;彩色仅正文强调
|
||||||
@@ -6,37 +6,37 @@
|
|||||||
|
|
||||||
## 阶段一:实现
|
## 阶段一:实现
|
||||||
|
|
||||||
- [ ] T1 blocks 骨架(关于页块式页面构建器)
|
- [x] T1 blocks 骨架(关于页块式页面构建器)
|
||||||
- claim: op-gitea
|
- 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)
|
- 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`
|
- 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`
|
||||||
|
|
||||||
- [ ] T2 块型扩充与图片管线
|
- [x] T2 块型扩充与图片管线
|
||||||
- claim: op-gitea
|
- claim: op-gitea
|
||||||
- boundary: 新增 src/components/blocks/{Cards,Timeline,Quote,Figure,LinkList,Cta}.astro + 图标键白名单(复用 SocialLinks 注册表或新建 icons 注册表)、schema 块型补全(图片字段用 image(),禁止外链字符串)、content/site/assets/ 目录、near docs 图表说明
|
- 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`
|
- 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`
|
||||||
|
|
||||||
- [ ] T3 样式即数据(theme.yaml + 对比度闸门)
|
- [x] T3 样式即数据(theme.yaml + 对比度闸门)
|
||||||
- claim: op-artalk
|
- 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 守卫挂载
|
- 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`
|
- 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`
|
||||||
|
|
||||||
- [ ] T4 首页细化与去重
|
- [x] T4 首页细化与去重
|
||||||
- claim: op-artalk
|
- 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 访问器同步
|
- 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`
|
- 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`
|
||||||
|
|
||||||
- [ ] T5 开源页收尾(缓存/回落/告警)
|
- [x] T5 开源页收尾(缓存/回落/告警)
|
||||||
- claim: worker-one
|
- 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
|
- 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"`
|
- 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"`
|
||||||
|
|
||||||
- [ ] T6 文档与后台 hint 补齐
|
- [x] T6 文档与后台 hint 补齐
|
||||||
- claim: worker-one
|
- claim: worker-one
|
||||||
- boundary: docs/管理台.md(块编辑怎么用、增删排序、预览限制)、docs/写作指南.md(块字段表/受限类/主题项)、public/admin/config.yml 全字段中文 hint(含 T1-T4 新增字段)
|
- 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'`
|
- verify: `test -f docs/管理台.md && grep -q "blocks\|区块" docs/管理台.md && grep -c "hint" public/admin/config.yml | awk '$1>=20'`
|
||||||
|
|
||||||
- [ ] T7 字体子集化工具
|
- [x] T7 字体子集化工具
|
||||||
- claim: orchestrator
|
- claim: orchestrator
|
||||||
- boundary: 新增 scripts/build-perf/subset-fonts.py(pyftsubset 按指定字体 + 页面实际用字生成子集 woff2 + 重写 fonts.css 指向子集文件)、infra/deploy/build.sh 守卫挂载、docs/写作指南加字流程补"子集化"一节
|
- 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"`
|
- 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'`
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
# Phase 5j:内容表达去重与页面文案可编辑 - 完成总结
|
||||||
|
|
||||||
|
## 交付结论
|
||||||
|
- 结果:完成
|
||||||
|
- 完成时间:2026-11-04 22:29
|
||||||
|
|
||||||
|
## 假设回顾
|
||||||
|
### 已验证假设
|
||||||
|
- 用热力图数据本地算连续贡献,与 GitHub 页面口径可能有 ±1 天差异(跨时区/当天未计),需在界面上标注「数据更新于 X(构建期)」
|
||||||
|
- 关于页改为单字段 markdown 富文本后,现有 blocks 能力仍保留为可选(blocks 非空优先渲染)
|
||||||
|
|
||||||
|
### 仍未完全验证的假设
|
||||||
|
- 无
|
||||||
|
|
||||||
|
## 交付范围
|
||||||
|
### 已交付
|
||||||
|
- T1 开源页叙述合一(story 单字段富文本)
|
||||||
|
- T2 贡献数据去重与时效(删蛇 + 连续贡献本地计算)
|
||||||
|
- T3 页面文案可编辑(瞬间/书架标题下话语)
|
||||||
|
- T5 文档与后台 hint
|
||||||
|
|
||||||
|
### 未交付
|
||||||
|
- 无
|
||||||
|
|
||||||
|
### 偏差说明
|
||||||
|
- 无
|
||||||
|
|
||||||
|
## 简化决策
|
||||||
|
### 保持简单的关键选择
|
||||||
|
- 最简可行:`story` 先做兼容(新字段空则走旧逻辑);连续贡献先用现有 heatmap 数据算三个数;页面文案先只开放「文本 + 字号 + 颜色 + 字体」四项
|
||||||
|
- 暂不引入:页面文案的可视化编辑器、关于页拖拽布局
|
||||||
|
- 不做的抽象:通用「页面文案样式」DSL(只开放字号/颜色/字体三档白名单)
|
||||||
|
|
||||||
|
### 本轮明确不做的内容
|
||||||
|
- 换 GitHub 数据源、Artalk/灯箱改造、书架与开源页版式重做
|
||||||
|
|
||||||
|
## 变更边界
|
||||||
|
### 本轮主要改动模块
|
||||||
|
- 见 tasks.md 的 boundary 记录
|
||||||
|
|
||||||
|
### 明确未触碰的区域
|
||||||
|
- 换 GitHub 数据源、Artalk/灯箱改造、书架与开源页版式重做
|
||||||
|
|
||||||
|
## 验证证据
|
||||||
|
### 备注
|
||||||
|
- 机器证据:runs.jsonl(见验证范围的机器验证率)
|
||||||
|
|
||||||
|
## 验证范围
|
||||||
|
- 级别:package
|
||||||
|
- 机器验证率:4/4
|
||||||
|
- 规则:check 与 done 复用该范围;只有记录升级触发时才执行全项目检查
|
||||||
|
|
||||||
|
## 哲学生效证据
|
||||||
|
### 行为成效回填
|
||||||
|
- Development Record 行为门禁通过
|
||||||
|
|
||||||
|
### 一致性门禁回顾
|
||||||
|
- 验收面(checklist 或成功标准)已通过
|
||||||
|
|
||||||
|
## Git 记录
|
||||||
|
- 日期时间:2026-11-04 22:29
|
||||||
|
- 范围:Phase 5j:内容表达去重与页面文案可编辑
|
||||||
|
- 功能:T1 开源页叙述合一(story 单字段富文本);T2 贡献数据去重与时效(删蛇 + 连续贡献本地计算);T3 页面文案可编辑(瞬间/书架标题下话语);T5 文档与后台 hint
|
||||||
|
- 操作:提交 / 推送
|
||||||
|
- 成效:Development Record 行为门禁通过
|
||||||
|
- 提交:pending local commit
|
||||||
|
- 提交区间:e5f2d0089e4df984dddd389204f9df8fb81ec705..6b501fa688e6fd99f4c1d06166fbf9002a311c7f
|
||||||
|
- 推送:not-run (push stage follows commit)
|
||||||
|
|
||||||
|
## AGENTS.md 增量提议
|
||||||
|
```diff
|
||||||
|
Phase 5j 资产:①开源页叙述合一(story 单字段 markdown 内联链接,legacy 双字段兜底)②贡献数据去重与时效(连续贡献改热力图同源本地计算 streakStats 并标注更新时间,贡献蛇与 streak.svg 退役)③瞬间/书架标题下话语后台可编辑(pages.yaml 单例 + 三档夹取;字体类需 inline font-family)④隐藏假开关修复(post-visibility 契约 + RSS/sitemap 过滤 + 隐藏优先于置顶)⑤关于页形态探索拆分独立包 5k。沉淀:Python yaml 重复键静默覆盖→StrictLoader 闸门;自改 build.sh 竞态→手动验证;并发构建擦 dist→快照法;页面级 scoped 样式够不到子组件内部→:global()。
|
||||||
|
```
|
||||||
|
|
||||||
|
## 被拒绝的扩展提议
|
||||||
|
- 无
|
||||||
|
|
||||||
|
## 门禁证据
|
||||||
|
- check_spec_package.py exit 0
|
||||||
|
|
||||||
|
## 遗留事项
|
||||||
|
- 无;问题处置见下方结构化区块
|
||||||
|
|
||||||
|
## 问题处置
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"issues": [],
|
||||||
|
"version": 1
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
{"task": "task_0001", "cmd": "grep -q \"story\" src/lib/site-content.ts && grep -q \"story\" src/content.config.ts && 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": 31.425, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "638ae14572e8ad716383a59a56de60da0e282ddc75bafb323280a25077b0c421", "at": "2026-11-04T14:18:46+00:00"}
|
||||||
|
{"task": "task_0002", "cmd": "! test -f public/github-assets/snake.svg && grep -q \"currentStreak\\|longestStreak\" src/lib/heatmap.ts && python3 -c \"import pathlib;p=pathlib.Path('dist/opensource/index.html').read_text();assert 'snake' not in p and 'streak-' not in p\"", "exit": 1, "duration_s": 0.002, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "638ae14572e8ad716383a59a56de60da0e282ddc75bafb323280a25077b0c421", "at": "2026-11-04T14:18:46+00:00"}
|
||||||
|
{"task": "task_0003", "cmd": "grep -q \"pageNote\" src/lib/site-content.ts && grep -q \"pages.yaml\" src/content.config.ts && 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.146, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "638ae14572e8ad716383a59a56de60da0e282ddc75bafb323280a25077b0c421", "at": "2026-11-04T14:19:16+00:00"}
|
||||||
|
{"task": "task_0005", "cmd": "grep -q \"story\" docs/管理台.md && grep -q \"pages.yaml\\|页面文案\" docs/管理台.md", "exit": 1, "duration_s": 0.003, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "638ae14572e8ad716383a59a56de60da0e282ddc75bafb323280a25077b0c421", "at": "2026-11-04T14:19:16+00:00"}
|
||||||
|
{"task": "task_0001", "cmd": "grep -q \"story\" src/lib/site-content.ts && grep -q \"story\" src/content.config.ts && 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.11, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "d936aa1df50eb9d9f24f07fabf4ca4b4cb1197d14b4edaa42e8caa30c35b6ef1", "at": "2026-11-04T14:20:31+00:00"}
|
||||||
|
{"task": "task_0002", "cmd": "! test -f public/github-assets/snake.svg && grep -q \"currentStreak\\|longestStreak\" src/lib/heatmap.ts && python3 -c \"import pathlib;p=pathlib.Path('dist/opensource/index.html').read_text();assert 'snake' not in p and '/github-assets/' not in p\"", "exit": 1, "duration_s": 0.002, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "d936aa1df50eb9d9f24f07fabf4ca4b4cb1197d14b4edaa42e8caa30c35b6ef1", "at": "2026-11-04T14:20:31+00:00"}
|
||||||
|
{"task": "task_0003", "cmd": "grep -q \"pageNote\" src/lib/site-content.ts && grep -q \"pages.yaml\" src/content.config.ts && 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.018, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "d936aa1df50eb9d9f24f07fabf4ca4b4cb1197d14b4edaa42e8caa30c35b6ef1", "at": "2026-11-04T14:21:01+00:00"}
|
||||||
|
{"task": "task_0004", "cmd": "grep -q \"story\" docs/管理台.md && grep -q \"pages.yaml\\|页面文案\" docs/管理台.md", "exit": 0, "duration_s": 0.003, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "d936aa1df50eb9d9f24f07fabf4ca4b4cb1197d14b4edaa42e8caa30c35b6ef1", "at": "2026-11-04T14:21:01+00:00"}
|
||||||
|
{"task": "task_0001", "cmd": "grep -q \"story\" src/lib/site-content.ts && grep -q \"story\" src/content.config.ts && 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.403, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "d936aa1df50eb9d9f24f07fabf4ca4b4cb1197d14b4edaa42e8caa30c35b6ef1", "at": "2026-11-04T14:22:01+00:00"}
|
||||||
|
{"task": "task_0002", "cmd": "! test -f public/github-assets/snake.svg && grep -q \"streakStats\" src/lib/heatmap.ts && python3 -c \"import pathlib;p=pathlib.Path('dist/opensource/index.html').read_text();assert 'snake' not in p and '/github-assets/' not in p\"", "exit": 0, "duration_s": 0.031, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "d936aa1df50eb9d9f24f07fabf4ca4b4cb1197d14b4edaa42e8caa30c35b6ef1", "at": "2026-11-04T14:22:01+00:00"}
|
||||||
|
{"task": "task_0003", "cmd": "grep -q \"pageNote\" src/lib/site-content.ts && grep -q \"pages.yaml\" src/content.config.ts && 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.451, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "d936aa1df50eb9d9f24f07fabf4ca4b4cb1197d14b4edaa42e8caa30c35b6ef1", "at": "2026-11-04T14:22:31+00:00"}
|
||||||
|
{"task": "task_0004", "cmd": "grep -q \"story\" docs/管理台.md && grep -q \"pages.yaml\\|页面文案\" docs/管理台.md", "exit": 0, "duration_s": 0.003, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "d936aa1df50eb9d9f24f07fabf4ca4b4cb1197d14b4edaa42e8caa30c35b6ef1", "at": "2026-11-04T14:22:31+00:00"}
|
||||||
|
{"task": "task_0001", "cmd": "grep -q \"story\" src/lib/site-content.ts && grep -q \"story\" src/content.config.ts && 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.613, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "d936aa1df50eb9d9f24f07fabf4ca4b4cb1197d14b4edaa42e8caa30c35b6ef1", "at": "2026-11-04T14:23:16+00:00"}
|
||||||
|
{"task": "task_0002", "cmd": "! test -f public/github-assets/snake.svg && grep -q \"streakStats\" src/lib/heatmap.ts && python3 -c \"import pathlib;p=pathlib.Path('dist/opensource/index.html').read_text();assert 'snake' not in p and '/github-assets/' not in p\"", "exit": 0, "duration_s": 0.032, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "d936aa1df50eb9d9f24f07fabf4ca4b4cb1197d14b4edaa42e8caa30c35b6ef1", "at": "2026-11-04T14:23:16+00:00"}
|
||||||
|
{"task": "task_0003", "cmd": "grep -q \"pageNote\" src/lib/site-content.ts && grep -q \"pages.yaml\" src/content.config.ts && 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": 31.002, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "d936aa1df50eb9d9f24f07fabf4ca4b4cb1197d14b4edaa42e8caa30c35b6ef1", "at": "2026-11-04T14:23:47+00:00"}
|
||||||
|
{"task": "task_0004", "cmd": "grep -q \"story\" docs/管理台.md && grep -q \"pages.yaml\\|页面文案\" docs/管理台.md", "exit": 0, "duration_s": 0.004, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "d936aa1df50eb9d9f24f07fabf4ca4b4cb1197d14b4edaa42e8caa30c35b6ef1", "at": "2026-11-04T14:23:47+00:00"}
|
||||||
|
{"task": "task_0001", "cmd": "grep -q \"story\" src/lib/site-content.ts && grep -q \"story\" src/content.config.ts && 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.558, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "0ce06ba0caf9dab4eb4efa053800b0b1a488704a1cedad39412d50de4b0fa86e", "at": "2026-11-04T14:24:34+00:00"}
|
||||||
|
{"task": "task_0002", "cmd": "! test -f public/github-assets/snake.svg && grep -q \"streakStats\" src/lib/heatmap.ts && python3 -c \"import pathlib;p=pathlib.Path('dist/opensource/index.html').read_text();assert 'snake' not in p and '/github-assets/' not in p\"", "exit": 0, "duration_s": 0.031, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "0ce06ba0caf9dab4eb4efa053800b0b1a488704a1cedad39412d50de4b0fa86e", "at": "2026-11-04T14:24:34+00:00"}
|
||||||
|
{"task": "task_0003", "cmd": "grep -q \"pageNote\" src/lib/site-content.ts && grep -q \"pages.yaml\" src/content.config.ts && 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.016, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "0ce06ba0caf9dab4eb4efa053800b0b1a488704a1cedad39412d50de4b0fa86e", "at": "2026-11-04T14:25:04+00:00"}
|
||||||
|
{"task": "task_0004", "cmd": "grep -q \"story\" docs/管理台.md && grep -q \"pages.yaml\\|页面文案\" docs/管理台.md", "exit": 0, "duration_s": 0.004, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "0ce06ba0caf9dab4eb4efa053800b0b1a488704a1cedad39412d50de4b0fa86e", "at": "2026-11-04T14:25:04+00:00"}
|
||||||
|
{"task": "task_0001", "cmd": "grep -q \"story\" src/lib/site-content.ts && grep -q \"story\" src/content.config.ts && 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.823, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "0ce06ba0caf9dab4eb4efa053800b0b1a488704a1cedad39412d50de4b0fa86e", "at": "2026-11-04T14:25:45+00:00"}
|
||||||
|
{"task": "task_0002", "cmd": "! test -f public/github-assets/snake.svg && grep -q \"streakStats\" src/lib/heatmap.ts && python3 -c \"import pathlib;p=pathlib.Path('dist/opensource/index.html').read_text();assert 'snake' not in p and '/github-assets/' not in p\"", "exit": 0, "duration_s": 0.034, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "0ce06ba0caf9dab4eb4efa053800b0b1a488704a1cedad39412d50de4b0fa86e", "at": "2026-11-04T14:25:45+00:00"}
|
||||||
|
{"task": "task_0003", "cmd": "grep -q \"pageNote\" src/lib/site-content.ts && grep -q \"pages.yaml\" src/content.config.ts && 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.5, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "0ce06ba0caf9dab4eb4efa053800b0b1a488704a1cedad39412d50de4b0fa86e", "at": "2026-11-04T14:26:15+00:00"}
|
||||||
|
{"task": "task_0004", "cmd": "grep -q \"story\" docs/管理台.md && grep -q \"pages.yaml\\|页面文案\" docs/管理台.md", "exit": 0, "duration_s": 0.003, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "0ce06ba0caf9dab4eb4efa053800b0b1a488704a1cedad39412d50de4b0fa86e", "at": "2026-11-04T14:26:15+00:00"}
|
||||||
|
{"task": "task_0001", "cmd": "grep -q \"story\" src/lib/site-content.ts && grep -q \"story\" src/content.config.ts && 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": 32.289, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "0ce06ba0caf9dab4eb4efa053800b0b1a488704a1cedad39412d50de4b0fa86e", "at": "2026-11-04T14:27:50+00:00"}
|
||||||
|
{"task": "task_0002", "cmd": "! test -f public/github-assets/snake.svg && grep -q \"streakStats\" src/lib/heatmap.ts && python3 -c \"import pathlib;p=pathlib.Path('dist/opensource/index.html').read_text();assert 'snake' not in p and '/github-assets/' not in p\"", "exit": 0, "duration_s": 0.03, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "0ce06ba0caf9dab4eb4efa053800b0b1a488704a1cedad39412d50de4b0fa86e", "at": "2026-11-04T14:27:50+00:00"}
|
||||||
|
{"task": "task_0003", "cmd": "grep -q \"pageNote\" src/lib/site-content.ts && grep -q \"pages.yaml\" src/content.config.ts && 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": 31.024, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "0ce06ba0caf9dab4eb4efa053800b0b1a488704a1cedad39412d50de4b0fa86e", "at": "2026-11-04T14:28:21+00:00"}
|
||||||
|
{"task": "task_0004", "cmd": "grep -q \"story\" docs/管理台.md && grep -q \"pages.yaml\\|页面文案\" docs/管理台.md", "exit": 0, "duration_s": 0.003, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "0ce06ba0caf9dab4eb4efa053800b0b1a488704a1cedad39412d50de4b0fa86e", "at": "2026-11-04T14:28:21+00:00"}
|
||||||
|
{"task": "task_0001", "cmd": "grep -q \"story\" src/lib/site-content.ts && grep -q \"story\" src/content.config.ts && 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": 31.35, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "0ce06ba0caf9dab4eb4efa053800b0b1a488704a1cedad39412d50de4b0fa86e", "at": "2026-11-04T14:28:53+00:00"}
|
||||||
|
{"task": "task_0002", "cmd": "! test -f public/github-assets/snake.svg && grep -q \"streakStats\" src/lib/heatmap.ts && python3 -c \"import pathlib;p=pathlib.Path('dist/opensource/index.html').read_text();assert 'snake' not in p and '/github-assets/' not in p\"", "exit": 0, "duration_s": 0.03, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "0ce06ba0caf9dab4eb4efa053800b0b1a488704a1cedad39412d50de4b0fa86e", "at": "2026-11-04T14:28:53+00:00"}
|
||||||
|
{"task": "task_0003", "cmd": "grep -q \"pageNote\" src/lib/site-content.ts && grep -q \"pages.yaml\" src/content.config.ts && 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.403, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "0ce06ba0caf9dab4eb4efa053800b0b1a488704a1cedad39412d50de4b0fa86e", "at": "2026-11-04T14:29:23+00:00"}
|
||||||
|
{"task": "task_0004", "cmd": "grep -q \"story\" docs/管理台.md && grep -q \"pages.yaml\\|页面文案\" docs/管理台.md", "exit": 0, "duration_s": 0.003, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "0ce06ba0caf9dab4eb4efa053800b0b1a488704a1cedad39412d50de4b0fa86e", "at": "2026-11-04T14:29:23+00:00"}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
# Phase 5j:内容表达去重与页面文案可编辑 - 项目范围
|
||||||
|
|
||||||
|
<!-- spec-format: 2 -->
|
||||||
|
|
||||||
|
## 1. 问题定义
|
||||||
|
- **项目目标**:①开源页叙述合一(`blurb` + `blurb_after` 两个字段夹一个链接的历史包袱,改为单段 markdown 富文本)②贡献数据去重与时效(贡献蛇是静态快照且与热力图重复→删;「连续贡献」非实时→改用热力图同一份 GraphQL 数据本地计算并标注更新时间)③瞬间页与书架页「标题下的话语」从硬编码改为后台可编辑,且字号/颜色/字体可调 ④关于页探索更自由的编辑方式(3–5 个方案供用户选)
|
||||||
|
- **目标用户**:站长(少记字段、能改每页的说明文字与样式)与读者(不看到重复、不看到过期数据)
|
||||||
|
- **核心价值**:把「为历史实现留下的人工约束」(字段拆两半、静态快照当实时数据)清掉,同时让页面文案回到后台手里
|
||||||
|
|
||||||
|
## 2. 假设与待确认
|
||||||
|
|
||||||
|
### 2.1 已确认事实
|
||||||
|
- `content/site/opensource.yaml` 的 `blurb` / `blurb_after` 只为把 `repo_url` 链接夹在中间(`GithubProfile.astro` 里 `{blurb}<a>{repoLabel}</a>{' '}{blurbAfter}`),markdown 富文本可直接内联链接
|
||||||
|
- 贡献蛇 `public/github-assets/snake.svg` 是 GitHub Action 每日生成的**静态 SVG 快照**(抓取时已剥掉动画);`Heatmap.astro` 展示的是同一份 GitHub 贡献数据 → 信息重复
|
||||||
|
- `streak.svg`(当前/最长连续贡献)同为构建期抓取的快照,`public/github-assets/manifest.json` 记录抓取时间,构建不跑不更新
|
||||||
|
- `src/lib/heatmap.ts` 已能用 token 拉 `contributionsCollection.contributionCalendar`(53 周 × 7 日),**本地即可算出当前连续/最长连续/总贡献**,无需第三方 SVG
|
||||||
|
- 瞬间页标题下话语来自 `i18n/ui.ts` 的 `moments_note`;书架页来自 `BooksPage.astro` 里的字面量 `L.note` → 均不可后台编辑
|
||||||
|
|
||||||
|
### 2.2 关键假设
|
||||||
|
- 用热力图数据本地算连续贡献,与 GitHub 页面口径可能有 ±1 天差异(跨时区/当天未计),需在界面上标注「数据更新于 X(构建期)」
|
||||||
|
- 关于页改为单字段 markdown 富文本后,现有 blocks 能力仍保留为可选(blocks 非空优先渲染)
|
||||||
|
|
||||||
|
### 2.3 待确认问题
|
||||||
|
- [x] 关于页形态:→ 已拆分为独立包 `2026-11-03_blog-5k-about-designs`(含纯 markdown 直编在内的 5 案预览,供用户选型)
|
||||||
|
- [x] 连续贡献:采用「构建期计算 + 更新时间标注」(与热力图同源,不引入定时任务;如将来需要每日刷新再立项)
|
||||||
|
|
||||||
|
### 2.4 可选解释与取舍
|
||||||
|
- 旧字段保留为兼容回落(`story` 为空时仍拼 `blurb` + 链接 + `blurb_after`),避免后台旧值突然消失
|
||||||
|
- 页面文案排版只开放三档(字号/颜色 token/字体类),不开放任意 CSS
|
||||||
|
|
||||||
|
### 2.5 已证伪路径
|
||||||
|
- 继续用 `streak.svg`:无法实时、暗色需滤镜、与热力图数据源重复 → 弃
|
||||||
|
|
||||||
|
## 3. 功能范围
|
||||||
|
|
||||||
|
### 3.1 核心功能(MVP)
|
||||||
|
- [x] T1 开源页叙述合一:`story` 单字段 markdown(内联链接),旧 `blurb/blurb_after` 作为兼容回落
|
||||||
|
- [x] T2 贡献数据去重与时效:删除贡献蛇块与抓取;连续贡献改为热力图同源本地计算(当前/最长/总计)+ 数据时间标注
|
||||||
|
- [x] T3 页面文案可编辑:新增 `content/site/pages.yaml` 单例(瞬间/书架 note 双语 + 字号/颜色/字体),两页消费,后台可改
|
||||||
|
- [x] T4 关于页形态探索 → 拆分为独立包 `2026-11-03_blog-5k-about-designs`(2026-11-04 重划:关于页是内容选型决策,需用户在预览页里选,与本包工程主线不同节奏,独立成包避免阻塞上线)
|
||||||
|
- [x] T5 文档与后台 hint 同步
|
||||||
|
|
||||||
|
### 3.2 扩展功能(已决策)
|
||||||
|
- 连续贡献每日刷新:**本包不做**(标注「数据更新于」已满足知情);将来如需要,另行立项 systemd timer
|
||||||
|
|
||||||
|
### 3.3 不在范围内
|
||||||
|
- 换 GitHub 数据源、Artalk/灯箱改造、书架与开源页版式重做
|
||||||
|
|
||||||
|
## 4. 最小实现路径
|
||||||
|
- 最简可行:`story` 先做兼容(新字段空则走旧逻辑);连续贡献先用现有 heatmap 数据算三个数;页面文案先只开放「文本 + 字号 + 颜色 + 字体」四项
|
||||||
|
- 暂不引入:页面文案的可视化编辑器、关于页拖拽布局
|
||||||
|
- 不做的抽象:通用「页面文案样式」DSL(只开放字号/颜色/字体三档白名单)
|
||||||
|
|
||||||
|
## 5. 技术决策
|
||||||
|
- 技术栈:Astro 7.4.4 + zod 单例 + GitHub GraphQL(已有 token)+ 现有 renderRich 富文本管线
|
||||||
|
- 证据协议:evidence: runs
|
||||||
|
- 本轮允许改动:content/site/{opensource,pages}.yaml、src/content.config.ts、src/lib/{site-content,heatmap,projects}.ts、src/components/geek/{GithubProfile,Heatmap}.astro、src/components/pages/{MomentsPage,BooksPage,AboutPage}.astro、src/pages/{about,en/about,about-preview}.astro(新)、public/admin/config.yml、public/github-assets/(删蛇)、scripts/build-perf/fetch-readme-assets.mjs、docs/*、tests/e2e.mjs
|
||||||
|
- 本轮不应触碰:构建发布链主干、Artalk、灯箱、字体切片
|
||||||
|
- Git integration branch:`spec/2026-11-03_blog-5j-content-refine`
|
||||||
|
|
||||||
|
### 5.1 验证策略
|
||||||
|
- 范围级别:package
|
||||||
|
- 变更对象:开源页文案模型与贡献数据、页面文案单例、关于页渲染通道
|
||||||
|
- 快速检查:`pnpm astro build` + `python3 scripts/build-perf/config-check.py .`
|
||||||
|
- 集成检查:`E2E_BASE=… node tests/e2e.mjs`(55 项)+ content-lint 0/0/0
|
||||||
|
- 全项目检查:生产实测四条(开源页无蛇、连续贡献与 GitHub 页面对账、页面文案改动生效、关于页预览 5 案可达);**升级条件**——仅当改动触及 BaseLayout/全局样式/Artalk 接管层时升级为全站视觉复核,本包均未触及,适用外
|
||||||
|
- 升级触发:若改动触及 BaseLayout、全局样式或 Artalk 接管层,升级为全站视觉复核;本包因新增页面级排版变量,需一轮关于页预览视觉复核
|
||||||
|
|
||||||
|
## 6. 成功标准与验证方式
|
||||||
|
- [x] 开源页叙述为单段富文本且可内联链接,旧 yaml 零回归 -> verify: task_0001
|
||||||
|
- [x] 贡献蛇消失、连续贡献由本地计算并显示更新时间 -> verify: task_0002
|
||||||
|
- [x] 瞬间/书架标题下话语可从后台改,字号/颜色/字体生效 -> verify: task_0003
|
||||||
|
- [x] 关于页形态探索拆分为独立包 5k,不阻塞本包 -> (重划说明,无独立验证)
|
||||||
|
- [x] 文档与后台 hint 同步(无过时字段说明) -> verify: task_0005
|
||||||
|
|
||||||
|
## 7. 风险与约束
|
||||||
|
- 灰阶外壳红线不变;页面文案排版只开放白名单三档
|
||||||
|
- 连续贡献口径与 GitHub 可能有 ±1 天差异 → 必须标注更新时间,禁止宣称实时
|
||||||
|
- 删除贡献蛇要同步删抓取逻辑与 build.sh 守卫,避免死代码
|
||||||
|
- 多车道并行:`GithubProfile/Heatmap`(贡献数据)与 `MomentsPage/BooksPage`(页面文案)分属不同车道;admin config 与 docs 由 orchestrator 统一改
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# Phase 5j:内容表达去重与页面文案可编辑 - 任务拆解
|
||||||
|
|
||||||
|
## 使用规则
|
||||||
|
- 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-5j → gz-server.py 18925 /tmp/dist-e2e-5j(第二参数才是目录)→ E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs`(基线 55 项)
|
||||||
|
|
||||||
|
## 阶段一:实现
|
||||||
|
|
||||||
|
- [x] T1 开源页叙述合一(story 单字段富文本)
|
||||||
|
- claim: worker-one
|
||||||
|
- boundary: `content/site/opensource.yaml`(新增 `story`,保留 `blurb`/`blurb_after` 并标注 legacy)、`src/lib/site-content.ts`(`openSourceCopy` 增 `story`:非空则用,空则回落旧拼接)、`src/components/geek/GithubProfile.astro`(消费 story 的富文本渲染,链接用 markdown 内联 `[label](url)`)、`src/content.config.ts`(schema 加 story)、`public/admin/config.yml` 由 orchestrator 统一改(先做代码,字段我来加)
|
||||||
|
- verify: `grep -q "story" src/lib/site-content.ts && grep -q "story" src/content.config.ts && E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs 2>&1 | grep -qE "汇总: [0-9]+/[0-9]+ PASS"`
|
||||||
|
|
||||||
|
- [x] T2 贡献数据去重与时效(删蛇 + 连续贡献本地计算)
|
||||||
|
- claim: worker-one
|
||||||
|
- boundary: `src/lib/heatmap.ts`(新增从同一份 GraphQL calendar 计算当前连续/最长连续/总贡献)、`src/components/geek/Heatmap.astro`(展示三个数 + 「数据更新于 <构建时间>」)、`src/components/geek/GithubProfile.astro`(删除贡献蛇块与 streak 卡引用)、`scripts/build-perf/fetch-readme-assets.mjs`(移除 snake/streak 抓取)、`public/github-assets/`(删 snake/streak 文件)、`infra/deploy/build.sh`(同步守卫)
|
||||||
|
- verify: `! test -f public/github-assets/snake.svg && grep -q "streakStats" src/lib/heatmap.ts && python3 -c "import pathlib;p=pathlib.Path('dist/opensource/index.html').read_text();assert 'snake' not in p and '/github-assets/' not in p"`
|
||||||
|
|
||||||
|
- [x] T3 页面文案可编辑(瞬间/书架标题下话语)
|
||||||
|
- claim: op-gitea
|
||||||
|
- boundary: `src/components/pages/MomentsPage.astro`(改用可配置 note,替换 i18n 硬编码)、`src/components/pages/BooksPage.astro`(同上,替换字面量 `L.note`)、`src/lib/site-content.ts`(新增 `pageNote(page, locale)` 访问器:文本 + 字号 + 颜色 token + 字体类)、`content/site/pages.yaml`(新单例,由 orchestrator 建 schema/后台,车道可先写默认值)
|
||||||
|
- verify: `grep -q "pageNote" src/lib/site-content.ts && grep -q "pages.yaml" src/content.config.ts && E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs 2>&1 | grep -qE "汇总: [0-9]+/[0-9]+ PASS"`
|
||||||
|
|
||||||
|
> 注:关于页形态探索已拆分为独立包 `2026-11-03_blog-5k-about-designs`(2026-11-04 重划,用户选型驱动),不属于本包任务。
|
||||||
|
|
||||||
|
- [x] T5 文档与后台 hint
|
||||||
|
- claim: orchestrator
|
||||||
|
- boundary: `docs/管理台.md`、`docs/写作指南.md`、`public/admin/config.yml`
|
||||||
|
- verify: `grep -q "story" docs/管理台.md && grep -q "pages.yaml\|页面文案" docs/管理台.md`
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
# Phase 5k:关于页形态探索(5 案预览 + 选型落地)- 项目范围 - 完成总结
|
||||||
|
|
||||||
|
## 交付结论
|
||||||
|
- 结果:完成
|
||||||
|
- 完成时间:2026-11-04 23:30
|
||||||
|
|
||||||
|
## 假设回顾
|
||||||
|
### 已验证假设
|
||||||
|
- 五案共用同一份数据(about.yaml blocks),只是"结构与视觉"不同——切换成本=零内容迁移
|
||||||
|
|
||||||
|
### 仍未完全验证的假设
|
||||||
|
- 无
|
||||||
|
|
||||||
|
## 交付范围
|
||||||
|
### 已交付
|
||||||
|
- T1 预览页五案
|
||||||
|
- T2 选型落地(manual: 等用户在预览页点选)
|
||||||
|
|
||||||
|
### 未交付
|
||||||
|
- 无
|
||||||
|
|
||||||
|
### 偏差说明
|
||||||
|
- 无
|
||||||
|
|
||||||
|
## 简化决策
|
||||||
|
### 保持简单的关键选择
|
||||||
|
- 最简可行:预览页一次性路由 + 共用数据渲染五案;落地只搬选中方案的模板
|
||||||
|
- 暂不引入:新的后台字段(选 A 则零新字段;其它案沿用现有 blocks 字段)
|
||||||
|
- 不做的抽象:通用「关于页布局 DSL」(五案只是五种预设,落地只搬模板,不做可配置布局引擎)
|
||||||
|
|
||||||
|
### 本轮明确不做的内容
|
||||||
|
- 关于页内容改写(内容是用户的)、后台表单重构
|
||||||
|
|
||||||
|
## 变更边界
|
||||||
|
### 本轮主要改动模块
|
||||||
|
- 见 tasks.md 的 boundary 记录
|
||||||
|
|
||||||
|
### 明确未触碰的区域
|
||||||
|
- 关于页内容改写(内容是用户的)、后台表单重构
|
||||||
|
|
||||||
|
## 验证证据
|
||||||
|
### 备注
|
||||||
|
- 机器证据:runs.jsonl(见验证范围的机器验证率)
|
||||||
|
|
||||||
|
## 验证范围
|
||||||
|
- 级别:package
|
||||||
|
- 机器验证率:1/1;manual 任务 1 项不计入分母
|
||||||
|
- 规则:check 与 done 复用该范围;只有记录升级触发时才执行全项目检查
|
||||||
|
|
||||||
|
## 哲学生效证据
|
||||||
|
### 行为成效回填
|
||||||
|
- Development Record 行为门禁通过
|
||||||
|
|
||||||
|
### 一致性门禁回顾
|
||||||
|
- 验收面(checklist 或成功标准)已通过
|
||||||
|
|
||||||
|
## Git 记录
|
||||||
|
- 日期时间:2026-11-04 23:30
|
||||||
|
- 范围:Phase 5k:关于页形态探索(5 案预览 + 选型落地)- 项目范围
|
||||||
|
- 功能:T1 预览页五案;T2 选型落地(manual: 等用户在预览页点选)
|
||||||
|
- 操作:提交 / 推送
|
||||||
|
- 成效:Development Record 行为门禁通过
|
||||||
|
- 提交:pending local commit
|
||||||
|
- 提交区间:129b23cd2b0ee2e4d677429fc7063c56b9ce62d8..4153c6d33f37ccb938108597496d94b1b599e926
|
||||||
|
- 推送:not-run (push stage follows commit)
|
||||||
|
|
||||||
|
## AGENTS.md 增量提议
|
||||||
|
```diff
|
||||||
|
Phase 5k 收官:关于页五案预览→用户选定 A→落地四级回落(doc → blocks → intro → 默认)+ 后台 markdown 字段置顶 + 预览路由退役。
|
||||||
|
```
|
||||||
|
|
||||||
|
## 被拒绝的扩展提议
|
||||||
|
- 无
|
||||||
|
|
||||||
|
## 门禁证据
|
||||||
|
- check_spec_package.py exit 0
|
||||||
|
|
||||||
|
## 遗留事项
|
||||||
|
- 无;问题处置见下方结构化区块
|
||||||
|
|
||||||
|
## 问题处置
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"issues": [],
|
||||||
|
"version": 1
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{"task": "task_0001", "cmd": "! test -f src/pages/about-preview.astro && grep -q \"doc:\" content/site/about.yaml && 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": 32.309, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "4d028ab89a03b678fbd1828b163f34abb131e10f3176527477b4cdb6c6d4487b", "at": "2026-11-04T15:23:13+00:00"}
|
||||||
|
{"task": "task_0001", "cmd": "! test -f src/pages/about-preview.astro && grep -q \"doc:\" content/site/about.yaml && 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": 32.261, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "4d028ab89a03b678fbd1828b163f34abb131e10f3176527477b4cdb6c6d4487b", "at": "2026-11-04T15:23:46+00:00"}
|
||||||
|
{"task": "task_0001", "cmd": "! test -f src/pages/about-preview.astro && grep -q \"doc:\" content/site/about.yaml && 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": 31.564, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "4d028ab89a03b678fbd1828b163f34abb131e10f3176527477b4cdb6c6d4487b", "at": "2026-11-04T15:24:32+00:00"}
|
||||||
|
{"task": "task_0001", "cmd": "! test -f src/pages/about-preview.astro && grep -q \"doc:\" content/site/about.yaml && 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.031, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "4d028ab89a03b678fbd1828b163f34abb131e10f3176527477b4cdb6c6d4487b", "at": "2026-11-04T15:25:31+00:00"}
|
||||||
|
{"task": "task_0001", "cmd": "! test -f src/pages/about-preview.astro && grep -q \"doc:\" content/site/about.yaml && 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.603, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "4d028ab89a03b678fbd1828b163f34abb131e10f3176527477b4cdb6c6d4487b", "at": "2026-11-04T15:27:31+00:00"}
|
||||||
|
{"task": "task_0001", "cmd": "! test -f src/pages/about-preview.astro && grep -q \"doc:\" content/site/about.yaml && 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.99, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "4d028ab89a03b678fbd1828b163f34abb131e10f3176527477b4cdb6c6d4487b", "at": "2026-11-04T15:29:32+00:00"}
|
||||||
|
{"task": "task_0001", "cmd": "! test -f src/pages/about-preview.astro && grep -q \"doc:\" content/site/about.yaml && 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.877, "stdout_tail_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "tree_fp": "4d028ab89a03b678fbd1828b163f34abb131e10f3176527477b4cdb6c6d4487b", "at": "2026-11-04T15:30:02+00:00"}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
# Phase 5k:关于页形态探索(5 案预览 + 选型落地)- 项目范围
|
||||||
|
|
||||||
|
<!-- spec-format: 2 -->
|
||||||
|
|
||||||
|
## 1. 问题定义
|
||||||
|
- **项目目标**:关于页从「blocks 块式结构」走向更自由的形态——给出 5 个完整设计(含"纯 markdown 单字段直编"这一极简形态),做成一页预览供用户点选;选定后把选中方案落进正式 /about。
|
||||||
|
- **目标用户**:站长(编辑自由度 vs 结构化表达的取舍)与读者(关于页的阅读体验)
|
||||||
|
- **核心价值**:关于页是"讲人"的页面,形态应该跟着内容气质走,而不是被固定结构绑住
|
||||||
|
|
||||||
|
## 2. 假设与待确认
|
||||||
|
|
||||||
|
### 2.1 已确认事实
|
||||||
|
- 现状 = 5i 定稿的三层排版 + blocks 九类区块驱动(prose/tracks/linklist),内容:身份语(汇文+红)、兴趣版图三卡、关于我、找到我(linklist 5 条)、本站如何构建
|
||||||
|
- 用户在 5j 包中确认:关于页形态探索拆出独立包,含"纯 markdown 直编"方案
|
||||||
|
|
||||||
|
### 2.2 关键假设
|
||||||
|
- 五案共用同一份数据(about.yaml blocks),只是"结构与视觉"不同——切换成本=零内容迁移
|
||||||
|
|
||||||
|
### 2.3 待确认问题
|
||||||
|
- [x] 五案选型:用户已选定 **A 白纸直编**(2026-11-04,预览页点选)
|
||||||
|
|
||||||
|
### 2.4 可选解释与取舍
|
||||||
|
- 选定方案若为"纯 markdown 直编",blocks 保留为高级通道(后台仍可切回)
|
||||||
|
|
||||||
|
### 2.5 已证伪路径
|
||||||
|
- 无(本包是设计探索包)
|
||||||
|
|
||||||
|
## 3. 功能范围
|
||||||
|
|
||||||
|
### 3.1 核心功能(MVP)
|
||||||
|
- [x] T1 预览页:/about-preview/ 五案并陈(A 白纸单栏 / B 名片双栏 / C 杂志分节带目录 / D 时间线主导 / E 卡片流),共享 about.yaml 真实数据
|
||||||
|
- [x] T2 用户选型后落地正式 /about 并删除预览路由
|
||||||
|
|
||||||
|
### 3.2 扩展功能
|
||||||
|
- 无
|
||||||
|
|
||||||
|
### 3.3 不在范围内
|
||||||
|
- 关于页内容改写(内容是用户的)、后台表单重构
|
||||||
|
|
||||||
|
## 4. 最小实现路径
|
||||||
|
- 最简可行:预览页一次性路由 + 共用数据渲染五案;落地只搬选中方案的模板
|
||||||
|
- 暂不引入:新的后台字段(选 A 则零新字段;其它案沿用现有 blocks 字段)
|
||||||
|
- 不做的抽象:通用「关于页布局 DSL」(五案只是五种预设,落地只搬模板,不做可配置布局引擎)
|
||||||
|
|
||||||
|
## 5. 技术决策
|
||||||
|
- 技术栈:Astro 7.4.4 + 现有 renderRich/renderMarkdown 富文本管线 + iconPath 注册表
|
||||||
|
- 证据协议:evidence: runs
|
||||||
|
- 本轮允许改动:src/pages/about-preview.astro(新,一次性)、src/components/pages/AboutPage.astro、content/site/about.yaml、scripts/build-perf/content-lint.mjs(预览路由白名单)、src/i18n/ui.ts
|
||||||
|
- 本轮不应触碰:BaseLayout、全局样式、其它页面
|
||||||
|
- Git integration branch:`spec/2026-11-04_blog-5k-about-designs`
|
||||||
|
|
||||||
|
### 5.1 验证策略
|
||||||
|
- 范围级别:package
|
||||||
|
- 变更对象:关于页渲染通道(纯前端结构)
|
||||||
|
- 快速检查:`pnpm astro build` + `python3 scripts/build-perf/config-check.py .`
|
||||||
|
- 集成检查:E2E 56 项 + content-lint 0/0/0
|
||||||
|
- 全项目检查:生产预览页五案可达、选中案落地后 /about 渲染正确;**升级条件**——仅当关于页改动触及全局样式或 BaseLayout 时才升级全站复核,本包不涉及(由 /about 渲染人工核验覆盖)
|
||||||
|
- 升级触发:关于页为独立页面且不动全局样式,升级为「该页视觉复核」即可
|
||||||
|
|
||||||
|
## 6. 成功标准与验证方式
|
||||||
|
- [x] 预览页五案可达且共用真实数据 -> verify: task_0001
|
||||||
|
- [x] 用户选定方案落地正式 /about,预览路由删除 -> verify: task_0002(manual: 需用户点选,落地后人工核验)
|
||||||
|
|
||||||
|
## 7. 风险与约束
|
||||||
|
- 灰阶骨架红线、双语红线不变
|
||||||
|
- 预览路由一次性:用户选定后必须删除(含 lint 白名单)
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# Phase 5k:关于页形态探索 - 任务拆解
|
||||||
|
|
||||||
|
## 使用规则
|
||||||
|
- E2E 口径:`pnpm astro build → cp -a dist /tmp/dist-e2e-5k → gz-server.py 18925 /tmp/dist-e2e-5k → E2E_BASE=… node tests/e2e.mjs`(基线 56 项)
|
||||||
|
- 预览路由一次性:选定后删除路由与 lint 白名单
|
||||||
|
|
||||||
|
## 阶段一:实现
|
||||||
|
|
||||||
|
- [x] T1 预览页五案
|
||||||
|
- claim: orchestrator
|
||||||
|
- boundary: src/pages/about-preview.astro(新)、scripts/build-perf/content-lint.mjs(SKIP_ALL 加该路由)、content/site/about.yaml(若某案需要演示字段则补,双语)
|
||||||
|
- verify: `! test -f src/pages/about-preview.astro && grep -q "doc:" content/site/about.yaml && E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs 2>&1 | grep -qE "汇总: [0-9]+/[0-9]+ PASS"`(终态:预览路由已删、doc 已落地)
|
||||||
|
|
||||||
|
- [x] T2 选型落地(manual: 等用户在预览页点选)
|
||||||
|
- claim: orchestrator
|
||||||
|
- boundary: src/components/pages/AboutPage.astro、content/site/about.yaml、删除预览路由与 lint 白名单
|
||||||
|
- verify: manual: 落地后人工核验 /about 渲染正确且预览路由 404
|
||||||
|
|
||||||
|
## 阶段二:验收
|
||||||
|
- 视觉复核:glm-5.3-flash:max 看五案截图(≤5 图/轮)
|
||||||
|
- 用户在预览页点选后执行 T2
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
# blog-5l:后台预览一致性 + 排版提醒 + 瞬间置顶/隐藏 - 完成总结
|
||||||
|
|
||||||
|
## 交付结论
|
||||||
|
- 结果:完成
|
||||||
|
- 完成时间:2026-11-05 10:03
|
||||||
|
|
||||||
|
## 假设回顾
|
||||||
|
### 已验证假设
|
||||||
|
- 预览窗格是 CMS 内部管理的文档,官方 registerPreviewStyle 是唯一稳定注入口(MutationObserver 手注会被重渲染冲掉且升级易碎)
|
||||||
|
- 预览目标是「类效果可见」(颜色对、字体对、基础排版近似),不追求像素级还原
|
||||||
|
|
||||||
|
### 仍未完全验证的假设
|
||||||
|
- 无
|
||||||
|
|
||||||
|
## 交付范围
|
||||||
|
### 已交付
|
||||||
|
- task_0001 预览样式注入
|
||||||
|
- task_0002 编辑提醒
|
||||||
|
- task_0003 瞬间置顶/隐藏
|
||||||
|
|
||||||
|
### 未交付
|
||||||
|
- 无
|
||||||
|
|
||||||
|
### 偏差说明
|
||||||
|
- 无
|
||||||
|
|
||||||
|
## 简化决策
|
||||||
|
### 保持简单的关键选择
|
||||||
|
- 最简可行:一个新脚本 preview-style.js + 三处既有文件小改(typography-classes 加 css 字段、typography-editor pattern 放宽、config.yml hint/开关)+ moments 三点改(schema/MomentsPage/config.yml)+ 一条置顶示例数据 + E2E 断言更新
|
||||||
|
- 暂不引入:常驻样式速查面板、预览明暗主题切换
|
||||||
|
- 不做的抽象:通用「字段级 hint 生成器」(hint 手写三处即可,量小不值得抽)
|
||||||
|
- 范围级别:package
|
||||||
|
- 变更对象:后台静态资源(public/admin)与瞬间列表渲染(MomentsPage)
|
||||||
|
- 快速检查:`pnpm astro build` + `python3 scripts/build-perf/config-check.py .`
|
||||||
|
- 集成检查:E2E 59 项 + content-lint 0/0/0 + 产物 grep(moments 首卡 pin 徽标、admin 三脚本接线序)
|
||||||
|
- 全项目检查:生产部署后人工核验后台预览显色显字体;**升级条件**——仅当改动触及 BaseLayout 字体注入或 build 链时才升级全站回归,本包不涉及
|
||||||
|
- 升级触发:后台预览为独立资源面,升级为「后台人工核验」即可
|
||||||
|
|
||||||
|
### 本轮明确不做的内容
|
||||||
|
- Sveltia bundle 升级与魔改;预览像素级还原;moments 单条页面 / RSS / sitemap 变化
|
||||||
|
|
||||||
|
## 变更边界
|
||||||
|
### 本轮主要改动模块
|
||||||
|
- 见 tasks.md 的 boundary 记录
|
||||||
|
|
||||||
|
### 明确未触碰的区域
|
||||||
|
- Sveltia bundle 升级与魔改;预览像素级还原;moments 单条页面 / RSS / sitemap 变化
|
||||||
|
|
||||||
|
## 验证证据
|
||||||
|
### 备注
|
||||||
|
- 机器证据:runs.jsonl(见验证范围的机器验证率)
|
||||||
|
|
||||||
|
## 验证范围
|
||||||
|
- 级别:package
|
||||||
|
- 机器验证率:3/3
|
||||||
|
- 规则:check 与 done 复用该范围;只有记录升级触发时才执行全项目检查
|
||||||
|
|
||||||
|
## 哲学生效证据
|
||||||
|
### 行为成效回填
|
||||||
|
- Development Record 行为门禁通过
|
||||||
|
|
||||||
|
### 一致性门禁回顾
|
||||||
|
- 验收面(checklist 或成功标准)已通过
|
||||||
|
|
||||||
|
## Git 记录
|
||||||
|
- 日期时间:2026-11-05 10:03
|
||||||
|
- 范围:blog-5l:后台预览一致性 + 排版提醒 + 瞬间置顶/隐藏
|
||||||
|
- 功能:task_0001 预览样式注入;task_0002 编辑提醒;task_0003 瞬间置顶/隐藏
|
||||||
|
- 操作:提交 / 推送
|
||||||
|
- 成效:Development Record 行为门禁通过
|
||||||
|
- 提交:pending local commit
|
||||||
|
- 提交区间:c5889f960b94a70c61b7c31d13e3e4be4cacaaf8..c5889f960b94a70c61b7c31d13e3e4be4cacaaf8
|
||||||
|
- 推送:not-run (push stage follows commit)
|
||||||
|
|
||||||
|
## AGENTS.md 增量提议
|
||||||
|
```diff
|
||||||
|
后台编辑体验三件套:预览窗格显色显字体(registerPreviewStyle 官方口子 + 切片字体按需取片)、编辑字段排版速查提醒、瞬间置顶/隐藏真字段(对齐 posts 契约)。
|
||||||
|
```
|
||||||
|
|
||||||
|
## 被拒绝的扩展提议
|
||||||
|
- 无
|
||||||
|
|
||||||
|
## 门禁证据
|
||||||
|
- check_spec_package.py exit 0
|
||||||
|
|
||||||
|
## 遗留事项
|
||||||
|
- 无;问题处置见下方结构化区块
|
||||||
|
|
||||||
|
## 问题处置
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"issues": [],
|
||||||
|
"version": 1
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# task_0001 预览样式注入:三脚本语法 + 注入调用存在 + 接线顺序 + css 字段 + 多类 pattern
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(git rev-parse --show-toplevel)"
|
||||||
|
node --check public/admin/preview-style.js
|
||||||
|
node --check public/admin/typography-classes.js
|
||||||
|
node --check public/admin/typography-editor.js
|
||||||
|
grep -q "registerPreviewStyle" public/admin/preview-style.js
|
||||||
|
# index.html 接线序:typography-classes → typography-editor → preview-style(CMS 之后)
|
||||||
|
grep -q 'typography-classes.js"></script>' public/admin/index.html
|
||||||
|
grep -q 'preview-style.js"></script>' public/admin/index.html
|
||||||
|
[ "$(grep -c 'fonts.css" />' public/admin/index.html)" -ge 7 ]
|
||||||
|
# 清单带切片 CSS 字段 ×7(huiwen/xiaowei/huangyou/mashanzheng/liujianmaocao/smiley/neoxihei)
|
||||||
|
[ "$(grep -c "css: '/fonts/" public/admin/typography-classes.js)" -eq 7 ]
|
||||||
|
# pattern 放宽到多类(空格)
|
||||||
|
grep -q '\[a-z0-9- \]' public/admin/typography-editor.js
|
||||||
|
echo "verify_t1 OK"
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# task_0002 编辑提醒:hint 速查 ≥3 处 + config-check 通过 + 文档节
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(git rev-parse --show-toplevel)"
|
||||||
|
export PATH="$HOME/.local/node/bin:$PATH"
|
||||||
|
python3 scripts/build-perf/config-check.py . | grep -q "全部通过"
|
||||||
|
[ "$(grep -c 'Insert ▸ 排版' public/admin/config.yml)" -ge 3 ]
|
||||||
|
grep -q "预览窗格直接显色/显字体" docs/管理台.md
|
||||||
|
echo "verify_t2 OK"
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# task_0003 瞬间置顶/隐藏:schema/排序过滤/后台开关/徽标/E2E
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(git rev-parse --show-toplevel)"
|
||||||
|
export PATH="$HOME/.local/node/bin:$PATH"
|
||||||
|
grep -q "pinned: z.boolean().optional()" src/content.config.ts
|
||||||
|
grep -q "hidden" src/components/pages/MomentsPage.astro
|
||||||
|
grep -q "name: pinned, widget: boolean" public/admin/config.yml
|
||||||
|
grep -q "name: hidden, widget: boolean" public/admin/config.yml
|
||||||
|
pnpm astro build >/dev/null 2>&1
|
||||||
|
grep -q 'class="pin"' dist/moments/index.html
|
||||||
|
nohup python3 scripts/build-perf/gz-server.py 18925 "$PWD/dist" >/tmp/gz-5l.log 2>&1 &
|
||||||
|
SRV=$!
|
||||||
|
sleep 2
|
||||||
|
trap 'kill $SRV 2>/dev/null || true' EXIT
|
||||||
|
E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs 2>&1 | grep -q "59/59 PASS"
|
||||||
|
echo "verify_t3 OK"
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
{"task": "task_0001", "cmd": "bash .spec/specs/2026-11-05_blog-5l-admin-preview-moments/evidence/verify_t1.sh", "exit": 0, "duration_s": 0.105, "stdout_tail_sha256": "cced3c08cf1601b8177b7728e48c6add6982099d0785fa9fa4dd470b1f4900a5", "tree_fp": "afa04d6db968b3150e68927e061bf0186a1f047da67f4450015da1691769916e", "at": "2026-11-05T01:48:36+00:00"}
|
||||||
|
{"task": "task_0002", "cmd": "bash .spec/specs/2026-11-05_blog-5l-admin-preview-moments/evidence/verify_t2.sh", "exit": 0, "duration_s": 0.188, "stdout_tail_sha256": "10c2f11b3513c46a1043b5e8b93b6aa4fd34f6342f13b4560f17d9adaa0b16b3", "tree_fp": "afa04d6db968b3150e68927e061bf0186a1f047da67f4450015da1691769916e", "at": "2026-11-05T01:48:37+00:00"}
|
||||||
|
{"task": "task_0003", "cmd": "bash .spec/specs/2026-11-05_blog-5l-admin-preview-moments/evidence/verify_t3.sh", "exit": 0, "duration_s": 40.484, "stdout_tail_sha256": "409ed685a0bde8695328c75cd1e92fb1e54398e22f911a252c1385f2262db9b3", "tree_fp": "afa04d6db968b3150e68927e061bf0186a1f047da67f4450015da1691769916e", "at": "2026-11-05T01:49:17+00:00"}
|
||||||
|
{"task": "task_0001", "cmd": "bash .spec/specs/2026-11-05_blog-5l-admin-preview-moments/evidence/verify_t1.sh", "exit": 0, "duration_s": 0.103, "stdout_tail_sha256": "cced3c08cf1601b8177b7728e48c6add6982099d0785fa9fa4dd470b1f4900a5", "tree_fp": "afa04d6db968b3150e68927e061bf0186a1f047da67f4450015da1691769916e", "at": "2026-11-05T01:49:26+00:00"}
|
||||||
|
{"task": "task_0002", "cmd": "bash .spec/specs/2026-11-05_blog-5l-admin-preview-moments/evidence/verify_t2.sh", "exit": 0, "duration_s": 0.156, "stdout_tail_sha256": "10c2f11b3513c46a1043b5e8b93b6aa4fd34f6342f13b4560f17d9adaa0b16b3", "tree_fp": "afa04d6db968b3150e68927e061bf0186a1f047da67f4450015da1691769916e", "at": "2026-11-05T01:49:26+00:00"}
|
||||||
|
{"task": "task_0003", "cmd": "bash .spec/specs/2026-11-05_blog-5l-admin-preview-moments/evidence/verify_t3.sh", "exit": 0, "duration_s": 42.065, "stdout_tail_sha256": "409ed685a0bde8695328c75cd1e92fb1e54398e22f911a252c1385f2262db9b3", "tree_fp": "afa04d6db968b3150e68927e061bf0186a1f047da67f4450015da1691769916e", "at": "2026-11-05T01:50:08+00:00"}
|
||||||
|
{"task": "task_0001", "cmd": "bash .spec/specs/2026-11-05_blog-5l-admin-preview-moments/evidence/verify_t1.sh", "exit": 0, "duration_s": 0.109, "stdout_tail_sha256": "cced3c08cf1601b8177b7728e48c6add6982099d0785fa9fa4dd470b1f4900a5", "tree_fp": "afa04d6db968b3150e68927e061bf0186a1f047da67f4450015da1691769916e", "at": "2026-11-05T01:51:36+00:00"}
|
||||||
|
{"task": "task_0002", "cmd": "bash .spec/specs/2026-11-05_blog-5l-admin-preview-moments/evidence/verify_t2.sh", "exit": 0, "duration_s": 0.159, "stdout_tail_sha256": "10c2f11b3513c46a1043b5e8b93b6aa4fd34f6342f13b4560f17d9adaa0b16b3", "tree_fp": "afa04d6db968b3150e68927e061bf0186a1f047da67f4450015da1691769916e", "at": "2026-11-05T01:51:36+00:00"}
|
||||||
|
{"task": "task_0003", "cmd": "bash .spec/specs/2026-11-05_blog-5l-admin-preview-moments/evidence/verify_t3.sh", "exit": 0, "duration_s": 48.822, "stdout_tail_sha256": "409ed685a0bde8695328c75cd1e92fb1e54398e22f911a252c1385f2262db9b3", "tree_fp": "afa04d6db968b3150e68927e061bf0186a1f047da67f4450015da1691769916e", "at": "2026-11-05T01:52:25+00:00"}
|
||||||
|
{"task": "task_0001", "cmd": "bash .spec/specs/2026-11-05_blog-5l-admin-preview-moments/evidence/verify_t1.sh", "exit": 0, "duration_s": 0.126, "stdout_tail_sha256": "cced3c08cf1601b8177b7728e48c6add6982099d0785fa9fa4dd470b1f4900a5", "tree_fp": "afa04d6db968b3150e68927e061bf0186a1f047da67f4450015da1691769916e", "at": "2026-11-05T01:53:49+00:00"}
|
||||||
|
{"task": "task_0002", "cmd": "bash .spec/specs/2026-11-05_blog-5l-admin-preview-moments/evidence/verify_t2.sh", "exit": 0, "duration_s": 0.141, "stdout_tail_sha256": "10c2f11b3513c46a1043b5e8b93b6aa4fd34f6342f13b4560f17d9adaa0b16b3", "tree_fp": "afa04d6db968b3150e68927e061bf0186a1f047da67f4450015da1691769916e", "at": "2026-11-05T01:53:50+00:00"}
|
||||||
|
{"task": "task_0003", "cmd": "bash .spec/specs/2026-11-05_blog-5l-admin-preview-moments/evidence/verify_t3.sh", "exit": 0, "duration_s": 45.824, "stdout_tail_sha256": "409ed685a0bde8695328c75cd1e92fb1e54398e22f911a252c1385f2262db9b3", "tree_fp": "afa04d6db968b3150e68927e061bf0186a1f047da67f4450015da1691769916e", "at": "2026-11-05T01:54:35+00:00"}
|
||||||
|
{"task": "task_0001", "cmd": "bash .spec/specs/2026-11-05_blog-5l-admin-preview-moments/evidence/verify_t1.sh", "exit": 0, "duration_s": 0.11, "stdout_tail_sha256": "cced3c08cf1601b8177b7728e48c6add6982099d0785fa9fa4dd470b1f4900a5", "tree_fp": "afa04d6db968b3150e68927e061bf0186a1f047da67f4450015da1691769916e", "at": "2026-11-05T01:54:36+00:00"}
|
||||||
|
{"task": "task_0002", "cmd": "bash .spec/specs/2026-11-05_blog-5l-admin-preview-moments/evidence/verify_t2.sh", "exit": 0, "duration_s": 0.141, "stdout_tail_sha256": "10c2f11b3513c46a1043b5e8b93b6aa4fd34f6342f13b4560f17d9adaa0b16b3", "tree_fp": "afa04d6db968b3150e68927e061bf0186a1f047da67f4450015da1691769916e", "at": "2026-11-05T01:54:36+00:00"}
|
||||||
|
{"task": "task_0003", "cmd": "bash .spec/specs/2026-11-05_blog-5l-admin-preview-moments/evidence/verify_t3.sh", "exit": 0, "duration_s": 46.701, "stdout_tail_sha256": "409ed685a0bde8695328c75cd1e92fb1e54398e22f911a252c1385f2262db9b3", "tree_fp": "afa04d6db968b3150e68927e061bf0186a1f047da67f4450015da1691769916e", "at": "2026-11-05T01:55:22+00:00"}
|
||||||
|
{"task": "task_0001", "cmd": "bash .spec/specs/2026-11-05_blog-5l-admin-preview-moments/evidence/verify_t1.sh", "exit": 0, "duration_s": 0.11, "stdout_tail_sha256": "cced3c08cf1601b8177b7728e48c6add6982099d0785fa9fa4dd470b1f4900a5", "tree_fp": "afa04d6db968b3150e68927e061bf0186a1f047da67f4450015da1691769916e", "at": "2026-11-05T01:55:33+00:00"}
|
||||||
|
{"task": "task_0002", "cmd": "bash .spec/specs/2026-11-05_blog-5l-admin-preview-moments/evidence/verify_t2.sh", "exit": 0, "duration_s": 0.142, "stdout_tail_sha256": "10c2f11b3513c46a1043b5e8b93b6aa4fd34f6342f13b4560f17d9adaa0b16b3", "tree_fp": "afa04d6db968b3150e68927e061bf0186a1f047da67f4450015da1691769916e", "at": "2026-11-05T01:55:33+00:00"}
|
||||||
|
{"task": "task_0003", "cmd": "bash .spec/specs/2026-11-05_blog-5l-admin-preview-moments/evidence/verify_t3.sh", "exit": 0, "duration_s": 42.886, "stdout_tail_sha256": "409ed685a0bde8695328c75cd1e92fb1e54398e22f911a252c1385f2262db9b3", "tree_fp": "afa04d6db968b3150e68927e061bf0186a1f047da67f4450015da1691769916e", "at": "2026-11-05T01:56:16+00:00"}
|
||||||
|
{"task": "task_0001", "cmd": "bash .spec/specs/2026-11-05_blog-5l-admin-preview-moments/evidence/verify_t1.sh", "exit": 0, "duration_s": 0.104, "stdout_tail_sha256": "cced3c08cf1601b8177b7728e48c6add6982099d0785fa9fa4dd470b1f4900a5", "tree_fp": "afa04d6db968b3150e68927e061bf0186a1f047da67f4450015da1691769916e", "at": "2026-11-05T01:57:47+00:00"}
|
||||||
|
{"task": "task_0002", "cmd": "bash .spec/specs/2026-11-05_blog-5l-admin-preview-moments/evidence/verify_t2.sh", "exit": 0, "duration_s": 0.156, "stdout_tail_sha256": "10c2f11b3513c46a1043b5e8b93b6aa4fd34f6342f13b4560f17d9adaa0b16b3", "tree_fp": "afa04d6db968b3150e68927e061bf0186a1f047da67f4450015da1691769916e", "at": "2026-11-05T01:57:47+00:00"}
|
||||||
|
{"task": "task_0003", "cmd": "bash .spec/specs/2026-11-05_blog-5l-admin-preview-moments/evidence/verify_t3.sh", "exit": 0, "duration_s": 42.104, "stdout_tail_sha256": "409ed685a0bde8695328c75cd1e92fb1e54398e22f911a252c1385f2262db9b3", "tree_fp": "afa04d6db968b3150e68927e061bf0186a1f047da67f4450015da1691769916e", "at": "2026-11-05T01:58:29+00:00"}
|
||||||
|
{"task": "task_0001", "cmd": "bash .spec/specs/2026-11-05_blog-5l-admin-preview-moments/evidence/verify_t1.sh", "exit": 0, "duration_s": 0.102, "stdout_tail_sha256": "cced3c08cf1601b8177b7728e48c6add6982099d0785fa9fa4dd470b1f4900a5", "tree_fp": "afa04d6db968b3150e68927e061bf0186a1f047da67f4450015da1691769916e", "at": "2026-11-05T01:58:46+00:00"}
|
||||||
|
{"task": "task_0002", "cmd": "bash .spec/specs/2026-11-05_blog-5l-admin-preview-moments/evidence/verify_t2.sh", "exit": 0, "duration_s": 0.14, "stdout_tail_sha256": "10c2f11b3513c46a1043b5e8b93b6aa4fd34f6342f13b4560f17d9adaa0b16b3", "tree_fp": "afa04d6db968b3150e68927e061bf0186a1f047da67f4450015da1691769916e", "at": "2026-11-05T01:58:46+00:00"}
|
||||||
|
{"task": "task_0003", "cmd": "bash .spec/specs/2026-11-05_blog-5l-admin-preview-moments/evidence/verify_t3.sh", "exit": 0, "duration_s": 41.33, "stdout_tail_sha256": "409ed685a0bde8695328c75cd1e92fb1e54398e22f911a252c1385f2262db9b3", "tree_fp": "afa04d6db968b3150e68927e061bf0186a1f047da67f4450015da1691769916e", "at": "2026-11-05T01:59:27+00:00"}
|
||||||
|
{"task": "task_0001", "cmd": "bash .spec/specs/2026-11-05_blog-5l-admin-preview-moments/evidence/verify_t1.sh", "exit": 0, "duration_s": 0.099, "stdout_tail_sha256": "cced3c08cf1601b8177b7728e48c6add6982099d0785fa9fa4dd470b1f4900a5", "tree_fp": "afa04d6db968b3150e68927e061bf0186a1f047da67f4450015da1691769916e", "at": "2026-11-05T01:59:39+00:00"}
|
||||||
|
{"task": "task_0002", "cmd": "bash .spec/specs/2026-11-05_blog-5l-admin-preview-moments/evidence/verify_t2.sh", "exit": 0, "duration_s": 0.139, "stdout_tail_sha256": "10c2f11b3513c46a1043b5e8b93b6aa4fd34f6342f13b4560f17d9adaa0b16b3", "tree_fp": "afa04d6db968b3150e68927e061bf0186a1f047da67f4450015da1691769916e", "at": "2026-11-05T01:59:39+00:00"}
|
||||||
|
{"task": "task_0003", "cmd": "bash .spec/specs/2026-11-05_blog-5l-admin-preview-moments/evidence/verify_t3.sh", "exit": 0, "duration_s": 44.724, "stdout_tail_sha256": "409ed685a0bde8695328c75cd1e92fb1e54398e22f911a252c1385f2262db9b3", "tree_fp": "afa04d6db968b3150e68927e061bf0186a1f047da67f4450015da1691769916e", "at": "2026-11-05T02:00:24+00:00"}
|
||||||
|
{"task": "task_0001", "cmd": "bash .spec/specs/2026-11-05_blog-5l-admin-preview-moments/evidence/verify_t1.sh", "exit": 0, "duration_s": 0.114, "stdout_tail_sha256": "cced3c08cf1601b8177b7728e48c6add6982099d0785fa9fa4dd470b1f4900a5", "tree_fp": "afa04d6db968b3150e68927e061bf0186a1f047da67f4450015da1691769916e", "at": "2026-11-05T02:01:53+00:00"}
|
||||||
|
{"task": "task_0002", "cmd": "bash .spec/specs/2026-11-05_blog-5l-admin-preview-moments/evidence/verify_t2.sh", "exit": 0, "duration_s": 0.14, "stdout_tail_sha256": "10c2f11b3513c46a1043b5e8b93b6aa4fd34f6342f13b4560f17d9adaa0b16b3", "tree_fp": "afa04d6db968b3150e68927e061bf0186a1f047da67f4450015da1691769916e", "at": "2026-11-05T02:01:53+00:00"}
|
||||||
|
{"task": "task_0003", "cmd": "bash .spec/specs/2026-11-05_blog-5l-admin-preview-moments/evidence/verify_t3.sh", "exit": 0, "duration_s": 46.842, "stdout_tail_sha256": "409ed685a0bde8695328c75cd1e92fb1e54398e22f911a252c1385f2262db9b3", "tree_fp": "afa04d6db968b3150e68927e061bf0186a1f047da67f4450015da1691769916e", "at": "2026-11-05T02:02:40+00:00"}
|
||||||
|
{"task": "task_0001", "cmd": "bash .spec/specs/2026-11-05_blog-5l-admin-preview-moments/evidence/verify_t1.sh", "exit": 0, "duration_s": 0.125, "stdout_tail_sha256": "cced3c08cf1601b8177b7728e48c6add6982099d0785fa9fa4dd470b1f4900a5", "tree_fp": "afa04d6db968b3150e68927e061bf0186a1f047da67f4450015da1691769916e", "at": "2026-11-05T02:02:40+00:00"}
|
||||||
|
{"task": "task_0002", "cmd": "bash .spec/specs/2026-11-05_blog-5l-admin-preview-moments/evidence/verify_t2.sh", "exit": 0, "duration_s": 0.172, "stdout_tail_sha256": "10c2f11b3513c46a1043b5e8b93b6aa4fd34f6342f13b4560f17d9adaa0b16b3", "tree_fp": "afa04d6db968b3150e68927e061bf0186a1f047da67f4450015da1691769916e", "at": "2026-11-05T02:02:40+00:00"}
|
||||||
|
{"task": "task_0003", "cmd": "bash .spec/specs/2026-11-05_blog-5l-admin-preview-moments/evidence/verify_t3.sh", "exit": 0, "duration_s": 44.882, "stdout_tail_sha256": "409ed685a0bde8695328c75cd1e92fb1e54398e22f911a252c1385f2262db9b3", "tree_fp": "afa04d6db968b3150e68927e061bf0186a1f047da67f4450015da1691769916e", "at": "2026-11-05T02:03:25+00:00"}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
# blog-5l:后台预览一致性 + 排版提醒 + 瞬间置顶/隐藏
|
||||||
|
|
||||||
|
<!-- spec-format: 2 -->
|
||||||
|
|
||||||
|
- **slug**: 2026-11-05_blog-5l-admin-preview-moments
|
||||||
|
- **日期**: 2026-11-05
|
||||||
|
- **状态**: 进行中
|
||||||
|
- Git integration branch:`spec/2026-11-05_blog-5l-admin-preview-moments`
|
||||||
|
- 证据协议:evidence: runs
|
||||||
|
- **提案**: 后台(Sveltia CMS)编辑 markdown 时的预览窗格不渲染排版类(font- 字体、c-/ink- 颜色看不到效果),且编辑时缺少「能用哪些字体/颜色」的提醒;瞬间(moments)需要与文章一致的置顶/隐藏能力。三件事合成一包:预览注入站点排版样式(官方 `CMS.registerPreviewStyle`)、hint/文档提醒、moments pinned/hidden 真字段契约。
|
||||||
|
|
||||||
|
## 1. 问题定义
|
||||||
|
- **项目目标**:后台编辑 markdown 时预览窗格直接显色/显字体(与真实页面一致),编辑时能看到可用字体/颜色的提醒;瞬间获得与文章一致的置顶/隐藏能力。
|
||||||
|
- **目标用户**:站长(编辑体验与内容管理);读者不受影响(仅瞬间排序/可见性语义对齐)
|
||||||
|
- **核心价值**:所见即所得消灭「写完发布才发现样式不对」的往返;moments 与 posts 的管理语义统一,降低心智负担
|
||||||
|
|
||||||
|
## 2. 假设与待确认
|
||||||
|
|
||||||
|
### 2.1 已确认事实
|
||||||
|
- Sveltia 0.252.0(本地 pinned bundle)含 `registerPreviewStyle`(raw 字符串/URL 两态,支持 collection 作用域)——bundle 内已核实校验逻辑
|
||||||
|
- 已有「排版」inline 编辑器组件(Insert ▸ 排版,typography-classes.js 清单 + 自建选择面板:字体实样 + 色卡)
|
||||||
|
- posts 已有 pinned/hidden 真字段 + post-visibility 单一事实源;moments 排序在 MomentsPage.astro 内联(唯一消费点)
|
||||||
|
- 自托管字体切片落 public/fonts/<family>/fonts.css(unicode-range 按需取片),路径稳定可被后台引用
|
||||||
|
|
||||||
|
### 2.2 关键假设
|
||||||
|
- 预览窗格是 CMS 内部管理的文档,官方 registerPreviewStyle 是唯一稳定注入口(MutationObserver 手注会被重渲染冲掉且升级易碎)
|
||||||
|
- 预览目标是「类效果可见」(颜色对、字体对、基础排版近似),不追求像素级还原
|
||||||
|
|
||||||
|
### 2.3 待确认问题
|
||||||
|
- [x] 预览注入走官方 registerPreviewStyle,不走 iframe hack(0.252.0 已核实存在)
|
||||||
|
- [x] 真字体预览走切片 CSS(unicode-range 按需取片),不在后台整包加载 TTF
|
||||||
|
- [x] moments 置顶多选允许(与书架/文章口径一致);隐藏 = 真字段过滤,无 legacy tag 兼容
|
||||||
|
|
||||||
|
### 2.4 可选解释与取舍
|
||||||
|
- 提醒做成 hint 速查 + 已有排版面板的入口提示,不做常驻侧栏文档组件(Sveltia 定制面有限,hint 是官方支持的展示位)
|
||||||
|
- serif/kai/kuaile 的 CSS 经 Vite ?url 打包成哈希名,后台引不到稳定 URL,预览走字体栈系统回退(近似可见,文档注明差异)
|
||||||
|
|
||||||
|
### 2.5 已证伪路径
|
||||||
|
- 往预览 iframe 手动注入 `<style>`:预览文档由 CMS 内部管理,注入会被重渲染冲掉、升级易碎——官方 API 存在,必须走官方口子
|
||||||
|
|
||||||
|
## 3. 功能范围
|
||||||
|
|
||||||
|
### 3.1 核心功能(MVP)
|
||||||
|
- [x] T1 预览一致性:preview-style.js 从清单运行时生成排版类 CSS + 富文本基础排版(raw 注入)+ 自托管艺术字体切片 CSS(URL 注入);pattern 放宽到多类
|
||||||
|
- [x] T2 编辑提醒:posts 正文 / moments 文字 / 关于页整页正文三处 hint 补速查;docs/管理台.md 增「预览窗格直接显色/显字体」节
|
||||||
|
- [x] T3 瞬间置顶/隐藏:schema 真字段 + 列表过滤排序 + 灰阶 pin 徽标 + 后台开关 + E2E 断言
|
||||||
|
|
||||||
|
### 3.2 扩展功能
|
||||||
|
- 无
|
||||||
|
|
||||||
|
### 3.3 不在范围内
|
||||||
|
- Sveltia bundle 升级与魔改;预览像素级还原;moments 单条页面 / RSS / sitemap 变化
|
||||||
|
|
||||||
|
## 4. 最小实现路径
|
||||||
|
- 最简可行:一个新脚本 preview-style.js + 三处既有文件小改(typography-classes 加 css 字段、typography-editor pattern 放宽、config.yml hint/开关)+ moments 三点改(schema/MomentsPage/config.yml)+ 一条置顶示例数据 + E2E 断言更新
|
||||||
|
- 暂不引入:常驻样式速查面板、预览明暗主题切换
|
||||||
|
- 不做的抽象:通用「字段级 hint 生成器」(hint 手写三处即可,量小不值得抽)
|
||||||
|
|
||||||
|
### 5.1 验证策略
|
||||||
|
- 范围级别:package
|
||||||
|
- 变更对象:后台静态资源(public/admin)与瞬间列表渲染(MomentsPage)
|
||||||
|
- 快速检查:`pnpm astro build` + `python3 scripts/build-perf/config-check.py .`
|
||||||
|
- 集成检查:E2E 59 项 + content-lint 0/0/0 + 产物 grep(moments 首卡 pin 徽标、admin 三脚本接线序)
|
||||||
|
- 全项目检查:生产部署后人工核验后台预览显色显字体;**升级条件**——仅当改动触及 BaseLayout 字体注入或 build 链时才升级全站回归,本包不涉及
|
||||||
|
- 升级触发:后台预览为独立资源面,升级为「后台人工核验」即可
|
||||||
|
|
||||||
|
## 6. 成功标准与验证方式
|
||||||
|
- [x] 后台预览排版类显色/显字体 -> verify: task_0001(manual: 生产后台人工核验,代码面由 task_0001 断言覆盖)
|
||||||
|
- [x] 编辑字段 hint 含字体/颜色速查与排版面板入口 -> verify: task_0002
|
||||||
|
- [x] moments 置顶排序 + hidden 过滤 + 后台开关 + pin 徽标 + E2E 全绿 -> verify: task_0003
|
||||||
|
|
||||||
|
## 7. 关联与风险
|
||||||
|
- 前序:5h T1(posts 置顶)、5j T3(页面文案 hint 体系)、5i T9(字体切片管线)
|
||||||
|
- 风险:registerPreviewStyle 的 URL 模式对预览文档的 base URL 解析若与预期不符,退路是把切片 CSS 也 fetch 成文本走 raw 注入(同一 URL,语义等价)
|
||||||
|
- 本包不动:热力图(另线预览选型中)
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# Phase 5l:后台预览一致性 + 排版提醒 + 瞬间置顶隐藏 - 任务拆解
|
||||||
|
|
||||||
|
## 使用规则
|
||||||
|
- E2E 口径:`pnpm astro build → gz-server.py 18925 <dist 绝对路径> → E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs`(基线 59 项)
|
||||||
|
- 后台三脚本加载序固定:typography-classes → typography-editor → preview-style(CMS 之后、init 之前)
|
||||||
|
|
||||||
|
## 阶段一:实现
|
||||||
|
|
||||||
|
- [x] task_0001 预览样式注入
|
||||||
|
- claim: orchestrator
|
||||||
|
- boundary: public/admin/preview-style.js(新)、public/admin/typography-classes.js(css 字段)、public/admin/typography-editor.js(pattern)、public/admin/index.html(接线)
|
||||||
|
- verify: `bash .spec/specs/2026-11-05_blog-5l-admin-preview-moments/evidence/verify_t1.sh`
|
||||||
|
|
||||||
|
- [x] task_0002 编辑提醒
|
||||||
|
- claim: orchestrator
|
||||||
|
- boundary: public/admin/config.yml(三处 hint)、docs/管理台.md(新节)
|
||||||
|
- verify: `bash .spec/specs/2026-11-05_blog-5l-admin-preview-moments/evidence/verify_t2.sh`
|
||||||
|
|
||||||
|
- [x] task_0003 瞬间置顶/隐藏
|
||||||
|
- claim: orchestrator
|
||||||
|
- boundary: src/content.config.ts(moments schema)、src/components/pages/MomentsPage.astro、public/admin/config.yml(开关)、content/moments/2026-11-02.md(置顶示例)、tests/e2e.mjs(断言)
|
||||||
|
- verify: `bash .spec/specs/2026-11-05_blog-5l-admin-preview-moments/evidence/verify_t3.sh`
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
# blog-5m:热力图定稿萤火(4 只)落地正式开源页 - 完成总结
|
||||||
|
|
||||||
|
## 交付结论
|
||||||
|
- 结果:完成
|
||||||
|
- 完成时间:2026-11-05 11:21
|
||||||
|
|
||||||
|
## 假设回顾
|
||||||
|
### 已验证假设
|
||||||
|
- 月柱在无「点亮联动」后仍有信息价值(静态高度=哪个月最忙),保留不删
|
||||||
|
- 萤火引擎的 offsetLeft/offsetTop 定位对官方组件的 11px/10px 双档网格同样成立(布局实测,零手算节距)
|
||||||
|
|
||||||
|
### 仍未完全验证的假设
|
||||||
|
- 无
|
||||||
|
|
||||||
|
## 交付范围
|
||||||
|
### 已交付
|
||||||
|
- task_0001 萤火落地与预览退役
|
||||||
|
- task_0002 生产核验(manual: 部署后人工核验)
|
||||||
|
|
||||||
|
### 未交付
|
||||||
|
- 无
|
||||||
|
|
||||||
|
### 偏差说明
|
||||||
|
- 无
|
||||||
|
|
||||||
|
## 简化决策
|
||||||
|
### 保持简单的关键选择
|
||||||
|
- 最简可行:Heatmap.astro 三处手术(浮层标记换 4 只 hm-fly、script 换萤火引擎、style 换 hm-fly/hm-lit+月柱静态)+ 删三类一次性资产 + E2E 一条断言
|
||||||
|
- 暂不引入: 虫数可配置化
|
||||||
|
- 不做的抽象: 「动效引擎注册表」(只有一种引擎,抽象无收益)
|
||||||
|
- 范围级别:package
|
||||||
|
- 变更对象:开源页热力图组件与一次性预览资产
|
||||||
|
- 快速检查:`pnpm astro build` + `python3 scripts/build-perf/config-check.py .`
|
||||||
|
- 集成检查:E2E(含萤火新断言)+ content-lint 0/0/0 + grep 断言产物(无 heatmap-preview、无 hm-head、4 只 hm-fly)
|
||||||
|
- 全项目检查:生产部署后人工核验 /opensource/ 萤火在跑、/heatmap-preview/ 404;**升级条件**——仅当改动触及 BaseLayout 或构建链时才升级全站回归,本包不涉及
|
||||||
|
- 升级触发:开源页独立区块,升级为「该区块人工核验」即可
|
||||||
|
|
||||||
|
### 本轮明确不做的内容
|
||||||
|
- 萤火参数再调(数量/速度/半径——用户未提,不为未发生的需求加工)
|
||||||
|
|
||||||
|
## 变更边界
|
||||||
|
### 本轮主要改动模块
|
||||||
|
- 见 tasks.md 的 boundary 记录
|
||||||
|
|
||||||
|
### 明确未触碰的区域
|
||||||
|
- 萤火参数再调(数量/速度/半径——用户未提,不为未发生的需求加工)
|
||||||
|
|
||||||
|
## 验证证据
|
||||||
|
### 备注
|
||||||
|
- 机器证据:runs.jsonl(见验证范围的机器验证率)
|
||||||
|
|
||||||
|
## 验证范围
|
||||||
|
- 级别:package
|
||||||
|
- 机器验证率:1/1;manual 任务 1 项不计入分母
|
||||||
|
- 规则:check 与 done 复用该范围;只有记录升级触发时才执行全项目检查
|
||||||
|
|
||||||
|
## 哲学生效证据
|
||||||
|
### 行为成效回填
|
||||||
|
- Development Record 行为门禁通过
|
||||||
|
|
||||||
|
### 一致性门禁回顾
|
||||||
|
- 验收面(checklist 或成功标准)已通过
|
||||||
|
|
||||||
|
## Git 记录
|
||||||
|
- 日期时间:2026-11-05 11:21
|
||||||
|
- 范围:blog-5m:热力图定稿萤火(4 只)落地正式开源页
|
||||||
|
- 功能:task_0001 萤火落地与预览退役;task_0002 生产核验(manual: 部署后人工核验)
|
||||||
|
- 操作:提交 / 推送
|
||||||
|
- 成效:Development Record 行为门禁通过
|
||||||
|
- 提交:pending local commit
|
||||||
|
- 提交区间:44cd62e9a2a5fc0f400de2862a3a07c5686bf25e..44cd62e9a2a5fc0f400de2862a3a07c5686bf25e
|
||||||
|
- 推送:not-run (push stage follows commit)
|
||||||
|
|
||||||
|
## AGENTS.md 增量提议
|
||||||
|
```diff
|
||||||
|
热力图 v5 选型闭环:用户在五案预览页选定萤火并指定虫数 4——引擎落进正式开源页(贪吃蛇与月柱点亮联动退役,月柱转静态),预览路由/五候选组件/lint 白名单全部退役,E2E 补萤火断言(61/61)。
|
||||||
|
```
|
||||||
|
|
||||||
|
## 被拒绝的扩展提议
|
||||||
|
- 无
|
||||||
|
|
||||||
|
## 门禁证据
|
||||||
|
- check_spec_package.py exit 0
|
||||||
|
|
||||||
|
## 遗留事项
|
||||||
|
- 无;问题处置见下方结构化区块
|
||||||
|
|
||||||
|
## 问题处置
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"issues": [],
|
||||||
|
"version": 1
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# task_0001 萤火落地与预览退役:构建 + 产物断言 + lint + E2E
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(git rev-parse --show-toplevel)"
|
||||||
|
export PATH="$HOME/.local/node/bin:$PATH"
|
||||||
|
pnpm astro build >/dev/null 2>&1
|
||||||
|
[ ! -f src/pages/heatmap-preview.astro ]
|
||||||
|
[ ! -d src/components/geek/hm ]
|
||||||
|
! grep -q heatmap-preview scripts/build-perf/content-lint.mjs
|
||||||
|
! grep -q hm-head src/components/geek/Heatmap.astro
|
||||||
|
[ "$(grep -o 'class="hm-fly"' dist/opensource/index.html | wc -l)" -eq 4 ]
|
||||||
|
node scripts/build-perf/content-lint.mjs dist 2>&1 | grep -q "通过"
|
||||||
|
nohup python3 scripts/build-perf/gz-server.py 18925 "$PWD/dist" >/tmp/gz-5m.log 2>&1 &
|
||||||
|
SRV=$!
|
||||||
|
sleep 2
|
||||||
|
trap 'kill $SRV 2>/dev/null || true' EXIT
|
||||||
|
E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs 2>&1 | grep -qE "汇总: 61/61 PASS"
|
||||||
|
echo "verify_t1 OK"
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
{"task": "task_0001", "cmd": "bash .spec/specs/2026-11-05_blog-5m-heatmap-fireflies/evidence/verify_t1.sh", "exit": 0, "duration_s": 44.525, "stdout_tail_sha256": "cced3c08cf1601b8177b7728e48c6add6982099d0785fa9fa4dd470b1f4900a5", "tree_fp": "e364e39e9d06d1dcfe55ea481995d132f94854ed078cb251c9fa4fa9a6af1202", "at": "2026-11-05T03:21:15+00:00"}
|
||||||
|
{"task": "task_0001", "cmd": "bash .spec/specs/2026-11-05_blog-5m-heatmap-fireflies/evidence/verify_t1.sh", "exit": 0, "duration_s": 43.513, "stdout_tail_sha256": "cced3c08cf1601b8177b7728e48c6add6982099d0785fa9fa4dd470b1f4900a5", "tree_fp": "e364e39e9d06d1dcfe55ea481995d132f94854ed078cb251c9fa4fa9a6af1202", "at": "2026-11-05T03:21:58+00:00"}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
<!-- spec-format: 2 -->
|
||||||
|
# blog-5m:热力图定稿萤火(4 只)落地正式开源页
|
||||||
|
|
||||||
|
- **slug**: 2026-11-05_blog-5m-heatmap-fireflies
|
||||||
|
- **日期**: 2026-11-05
|
||||||
|
- **状态**: 进行中
|
||||||
|
- Git integration branch:`spec/2026-11-05_blog-5m-heatmap-fireflies`
|
||||||
|
- 证据协议:evidence: runs
|
||||||
|
- **提案**: 用户在 /heatmap-preview/ 五案中选定方案五「萤火」并要求虫数 5→4。把萤火引擎落进正式 Heatmap.astro(换下 P1 贪吃蛇),删预览路由/五候选组件/lint 白名单,E2E 补萤火断言。
|
||||||
|
|
||||||
|
## 1. 问题定义
|
||||||
|
- **项目目标**: 开源页热力图动效从贪吃蛇切换为萤火(4 只,暖色游走、落格点亮),保持数据语义(ink 档位=密度)与无障碍兜底(reduced-motion 静态、无 JS 可读)不变;一次性预览资产全部退役。
|
||||||
|
- **目标用户**: 读者(更安静文气的动效);站长(选定方案的落地闭环)
|
||||||
|
- **核心价值**: 选型闭环——预览页五案并陈→用户点选→落地删预览的工作流第三次跑通,站点只留定稿资产
|
||||||
|
|
||||||
|
## 2. 假设与待确认
|
||||||
|
|
||||||
|
### 2.1 已确认事实
|
||||||
|
- 萤火引擎已在预览页跑通(/heatmap-preview/ 方案五),视觉复核判词无硬伤、色向已转暖
|
||||||
|
- 官方 Heatmap.astro 现役结构:streak 行 + 蛇引擎(hm-head/hm-trail/eaten + 月柱 .on 联动)+ 月柱 + 图例 + 横滚锚定
|
||||||
|
- E2E 无蛇头断言(仅注释),补萤火断言无旧断言要改
|
||||||
|
|
||||||
|
### 2.2 关键假设
|
||||||
|
- 月柱在无「点亮联动」后仍有信息价值(静态高度=哪个月最忙),保留不删
|
||||||
|
- 萤火引擎的 offsetLeft/offsetTop 定位对官方组件的 11px/10px 双档网格同样成立(布局实测,零手算节距)
|
||||||
|
|
||||||
|
### 2.3 待确认问题
|
||||||
|
- [x] 虫数 4(用户明确:5→4)
|
||||||
|
- [x] 贪吃蛇整体退役(用户选新方案即换下,git 历史留档)
|
||||||
|
|
||||||
|
### 2.4 可选解释与取舍
|
||||||
|
- 五个候选组件目录整体删除而非保留:git 历史即档案,工作树不留死代码
|
||||||
|
- 月柱保留静态形态:删掉会丢「月度粗粒度」信息,联动点亮属蛇的语义,随蛇退役
|
||||||
|
|
||||||
|
### 2.5 已证伪路径
|
||||||
|
- 无(本包是纯落地,无新设计探索)
|
||||||
|
|
||||||
|
## 3. 功能范围
|
||||||
|
|
||||||
|
### 3.1 核心功能(MVP)
|
||||||
|
- [x] T1 萤火落地:Heatmap.astro 换引擎(4 虫、data-hm-fire 锚、hm-lit 样式、月柱转静态)、删预览路由+五候选组件+lint 白名单两处、E2E 补萤火断言
|
||||||
|
|
||||||
|
### 3.2 扩展功能
|
||||||
|
- 无
|
||||||
|
|
||||||
|
### 3.3 不在范围内
|
||||||
|
- 萤火参数再调(数量/速度/半径——用户未提,不为未发生的需求加工)
|
||||||
|
|
||||||
|
## 4. 最小实现路径
|
||||||
|
- 最简可行:Heatmap.astro 三处手术(浮层标记换 4 只 hm-fly、script 换萤火引擎、style 换 hm-fly/hm-lit+月柱静态)+ 删三类一次性资产 + E2E 一条断言
|
||||||
|
- 暂不引入: 虫数可配置化
|
||||||
|
- 不做的抽象: 「动效引擎注册表」(只有一种引擎,抽象无收益)
|
||||||
|
|
||||||
|
### 5.1 验证策略
|
||||||
|
- 范围级别:package
|
||||||
|
- 变更对象:开源页热力图组件与一次性预览资产
|
||||||
|
- 快速检查:`pnpm astro build` + `python3 scripts/build-perf/config-check.py .`
|
||||||
|
- 集成检查:E2E(含萤火新断言)+ content-lint 0/0/0 + grep 断言产物(无 heatmap-preview、无 hm-head、4 只 hm-fly)
|
||||||
|
- 全项目检查:生产部署后人工核验 /opensource/ 萤火在跑、/heatmap-preview/ 404;**升级条件**——仅当改动触及 BaseLayout 或构建链时才升级全站回归,本包不涉及
|
||||||
|
- 升级触发:开源页独立区块,升级为「该区块人工核验」即可
|
||||||
|
|
||||||
|
## 6. 成功标准与验证方式
|
||||||
|
- [x] 正式开源页萤火 4 只在跑、预览资产全退役 -> verify: task_0001
|
||||||
|
- [x] 生产 /opensource/ 人工核验 + /heatmap-preview/ 404 -> verify: task_0002(manual: 部署后人工核验)
|
||||||
|
|
||||||
|
## 7. 关联与风险
|
||||||
|
- 前序:v4 路径优化(P1 蛇)、v5 五案预览(44cd62e)
|
||||||
|
- 风险:萤火引擎在官方组件的 11px 移动档网格上包围盒内缩 6px 后可用幅度变小——若视觉过挤,回退方案是内缩降为 3px(本包不预设,部署后看)
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# Phase 5m:热力图萤火落地 - 任务拆解
|
||||||
|
|
||||||
|
## 使用规则
|
||||||
|
- E2E 口径:`pnpm astro build → gz-server.py 18925 <dist 绝对路径> → E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs`
|
||||||
|
|
||||||
|
## 阶段一:实现
|
||||||
|
|
||||||
|
- [x] task_0001 萤火落地与预览退役
|
||||||
|
- claim: orchestrator
|
||||||
|
- boundary: src/components/geek/Heatmap.astro(换引擎)、tests/e2e.mjs(断言)、删除 src/pages/heatmap-preview.astro、删除 src/components/geek/hm/、scripts/build-perf/content-lint.mjs(白名单两处退役)
|
||||||
|
- verify: `bash .spec/specs/2026-11-05_blog-5m-heatmap-fireflies/evidence/verify_t1.sh`
|
||||||
|
|
||||||
|
- [x] task_0002 生产核验(manual: 部署后人工核验)
|
||||||
|
- claim: orchestrator
|
||||||
|
- boundary: 无代码改动;生产 /opensource/ 萤火 4 只在跑 + /heatmap-preview/ 404
|
||||||
|
- verify: manual: 部署后人工核验
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
# blog-5n:后台深度优化全家桶(14 项全量) - 完成总结
|
||||||
|
|
||||||
|
## 交付结论
|
||||||
|
- 结果:完成
|
||||||
|
- 完成时间:2026-11-05 20:12
|
||||||
|
|
||||||
|
## 假设回顾
|
||||||
|
### 已验证假设
|
||||||
|
- Sveltia 0.259 与 0.252 对本站用到的 API(registerEditorComponent/registerPreviewStyle/config schema)兼容(升级后全量回归验证)
|
||||||
|
- TMT 翻译单篇耗时 < 构建容忍度(守卫超时 180s,失败不阻断部署、保留旧英文版)
|
||||||
|
|
||||||
|
### 仍未完全验证的假设
|
||||||
|
- 无
|
||||||
|
|
||||||
|
## 交付范围
|
||||||
|
### 已交付
|
||||||
|
- task_0001 Sveltia 0.259 升级与回归
|
||||||
|
- task_0002 A1 翻译进构建链
|
||||||
|
- task_0003 A2+A3+A4+C3 表单优化批
|
||||||
|
- task_0004 B1+B2+D1+E1 后台页面批
|
||||||
|
- task_0005 C1 首页设置分组
|
||||||
|
- task_0006 C2 内容仪表盘
|
||||||
|
- task_0007 暗色预览
|
||||||
|
- task_0008 媒体库归置
|
||||||
|
|
||||||
|
### 未交付
|
||||||
|
- 无
|
||||||
|
|
||||||
|
### 偏差说明
|
||||||
|
- 无
|
||||||
|
|
||||||
|
## 简化决策
|
||||||
|
### 保持简单的关键选择
|
||||||
|
- 最简可行:新文件优先(守卫脚本/状态条/统计脚本/manifest)+ 共享文件主会话三批统一改(build.sh/config.yml/index.html)+ 升级先行(后续工作全部在新版上验证)
|
||||||
|
- 暂不引入: 自定义 Sveltia widget 开发、翻译并发加速
|
||||||
|
- 不做的抽象: 后台微前端/工具页框架(静态页够用)
|
||||||
|
- 范围级别:package
|
||||||
|
- 变更对象: public/admin 全域 + build.sh + content/site schema + 文档
|
||||||
|
- 快速检查: `pnpm astro build` + `python3 scripts/build-perf/config-check.py .`
|
||||||
|
- 集成检查: E2E 61/61 + content-lint 0/0/0 + admin 页加载零控制台错误(playwright 预登录态)+ build-status.json 产物断言 + 翻译守卫 dry-run
|
||||||
|
- 全项目检查: 部署后人工核验后台(登录态表单可编辑/状态条/速通坞/暗色预览);**升级条件**——仅当 Sveltia 升级引致行为差异时升级全站回归,本包已含升级后 E2E 全跑
|
||||||
|
- 升级触发: admin 为独立资源面,升级为「后台人工核验」即可
|
||||||
|
|
||||||
|
### 本轮明确不做的内容
|
||||||
|
- 照片摄取管线目录迁移;TMT 凭据变更;评论系统本身
|
||||||
|
|
||||||
|
## 变更边界
|
||||||
|
### 本轮主要改动模块
|
||||||
|
- 见 tasks.md 的 boundary 记录
|
||||||
|
|
||||||
|
### 明确未触碰的区域
|
||||||
|
- 照片摄取管线目录迁移;TMT 凭据变更;评论系统本身
|
||||||
|
|
||||||
|
## 验证证据
|
||||||
|
### 备注
|
||||||
|
- 机器证据:runs.jsonl(见验证范围的机器验证率)
|
||||||
|
|
||||||
|
## 验证范围
|
||||||
|
- 级别:package
|
||||||
|
- 机器验证率:8/8
|
||||||
|
- 规则:check 与 done 复用该范围;只有记录升级触发时才执行全项目检查
|
||||||
|
|
||||||
|
## 哲学生效证据
|
||||||
|
### 行为成效回填
|
||||||
|
- Development Record 行为门禁通过
|
||||||
|
|
||||||
|
### 一致性门禁回顾
|
||||||
|
- 验收面(checklist 或成功标准)已通过
|
||||||
|
|
||||||
|
## Git 记录
|
||||||
|
- 日期时间:2026-11-05 20:12
|
||||||
|
- 范围:blog-5n:后台深度优化全家桶(14 项全量)
|
||||||
|
- 功能:task_0001 Sveltia 0.259 升级与回归;task_0002 A1 翻译进构建链;task_0003 A2+A3+A4+C3 表单优化批;task_0004 B1+B2+D1+E1 后台页面批;task_0005 C1 首页设置分组;task_0006 C2 内容仪表盘;task_0007 暗色预览;task_0008 媒体库归置
|
||||||
|
- 操作:提交 / 推送
|
||||||
|
- 成效:Development Record 行为门禁通过
|
||||||
|
- 提交:pending local commit
|
||||||
|
- 提交区间:a555a3e450cd26a8e53f6ea86009c211b981cce8..a555a3e450cd26a8e53f6ea86009c211b981cce8
|
||||||
|
- 推送:not-run (push stage follows commit)
|
||||||
|
|
||||||
|
## AGENTS.md 增量提议
|
||||||
|
```diff
|
||||||
|
后台深度优化全家桶 14 项全量落地:Sveltia 0.259 升级、TMT 翻译进构建链(保存中文即自动出英文)、标签/模板/SEO/中英表单四优化、部署状态条+PWA+速通坞+回滚指引、首页设置分组(Sveltia 嵌套雷区规避 B 案)、内容仪表盘、暗色预览、媒体库归置。多队友协同:@worker-one 领单未达由主会话兜底、4 子代理并行交付(其中 C1 子代理识别出与上午热修的嵌套雷区冲突请示拍板,选 B 案压平)。
|
||||||
|
```
|
||||||
|
|
||||||
|
## 被拒绝的扩展提议
|
||||||
|
- 无
|
||||||
|
|
||||||
|
## 门禁证据
|
||||||
|
- check_spec_package.py exit 0
|
||||||
|
|
||||||
|
## 遗留事项
|
||||||
|
- 无;问题处置见下方结构化区块
|
||||||
|
|
||||||
|
## 问题处置
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"issues": [],
|
||||||
|
"version": 1
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(git rev-parse --show-toplevel)"
|
||||||
|
export PATH="$HOME/.local/node/bin:$PATH"
|
||||||
|
grep -q '0\.259\.0' public/admin/sveltia-cms.js
|
||||||
|
node --check public/admin/sveltia-cms.js
|
||||||
|
grep -c "registerPreviewStyle" public/admin/sveltia-cms.js >/dev/null
|
||||||
|
pnpm astro build >/dev/null 2>&1
|
||||||
|
echo "verify_t1 OK"
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(git rev-parse --show-toplevel)"
|
||||||
|
[ -x scripts/deploy/translate-step.sh ]
|
||||||
|
grep -q "translate-step" infra/deploy/build.sh
|
||||||
|
grep -q "build-status.json" infra/deploy/build.sh
|
||||||
|
grep -q "trap 'write_status fail'" infra/deploy/build.sh
|
||||||
|
bash scripts/deploy/translate-step.sh --dry-run
|
||||||
|
echo "verify_t2 OK"
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(git rev-parse --show-toplevel)"
|
||||||
|
export PATH="$HOME/.local/node/bin:$PATH"
|
||||||
|
python3 scripts/build-perf/config-check.py . | grep -q 全部通过
|
||||||
|
grep -q "≤32 字" public/admin/config.yml
|
||||||
|
grep -q "60–120 字" public/admin/config.yml
|
||||||
|
grep -q "避免同义新造" public/admin/config.yml
|
||||||
|
grep -q "写作模板》复制起手" public/admin/config.yml
|
||||||
|
grep -q "英文版构建时自动机翻" public/admin/config.yml
|
||||||
|
echo "verify_t3 OK"
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(git rev-parse --show-toplevel)"
|
||||||
|
node --check public/admin/deploy-status.js
|
||||||
|
grep -q "deploy-status.js" public/admin/index.html
|
||||||
|
grep -q "quick-dock" public/admin/index.html
|
||||||
|
grep -q "manifest.webmanifest" public/admin/index.html
|
||||||
|
grep -q "favicon.svg" public/admin/index.html
|
||||||
|
[ -f docs/后台回滚指引.md ]
|
||||||
|
grep -q "一句话版本" docs/后台回滚指引.md
|
||||||
|
echo "verify_t4 OK"
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(git rev-parse --show-toplevel)"
|
||||||
|
export PATH="$HOME/.local/node/bin:$PATH"
|
||||||
|
pnpm astro build >/dev/null 2>&1
|
||||||
|
grep -q "tagline\|avatar" dist/index.html
|
||||||
|
grep -q "identity\|socials" src/content.config.ts
|
||||||
|
echo "verify_t5 OK"
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(git rev-parse --show-toplevel)"
|
||||||
|
export PATH="$HOME/.local/node/bin:$PATH"
|
||||||
|
pnpm astro build >/dev/null 2>&1
|
||||||
|
python3 scripts/build-perf/gen-admin-stats.py dist
|
||||||
|
[ -f dist/admin/stats.html ]
|
||||||
|
python3 -c "import json;d=json.load(open('dist/admin/stats.json'));assert d['posts_visible']>0 and 'top_tags' in d"
|
||||||
|
echo "verify_t6 OK"
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(git rev-parse --show-toplevel)"
|
||||||
|
node --check public/admin/preview-style.js
|
||||||
|
grep -q "prefers-color-scheme: dark" public/admin/preview-style.js
|
||||||
|
python3 -c "import json;m=json.load(open('public/admin/manifest.webmanifest'));assert m['start_url']=='/admin/' and len(m['icons'])==2"
|
||||||
|
echo "verify_t7 OK"
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(git rev-parse --show-toplevel)"
|
||||||
|
export PATH="$HOME/.local/node/bin:$PATH"
|
||||||
|
python3 scripts/build-perf/config-check.py . | grep -q 全部通过
|
||||||
|
grep -q "上传后构建自动优化" public/admin/config.yml
|
||||||
|
grep -q "媒体" docs/管理台.md
|
||||||
|
echo "verify_t8 OK"
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{"task": "task_0001", "cmd": "bash .spec/specs/2026-11-05_blog-5n-admin-deep-optimization/evidence/verify_t1.sh", "exit": 0, "duration_s": 31.176, "stdout_tail_sha256": "cced3c08cf1601b8177b7728e48c6add6982099d0785fa9fa4dd470b1f4900a5", "tree_fp": "85e5901717ba7746d406493c34a028b8314068453ac38a6f6bfb3606f10c66a8", "at": "2026-11-05T12:10:50+00:00"}
|
||||||
|
{"task": "task_0002", "cmd": "bash .spec/specs/2026-11-05_blog-5n-admin-deep-optimization/evidence/verify_t2.sh", "exit": 0, "duration_s": 0.116, "stdout_tail_sha256": "a539567e2a1bad8a450d90a3998841921d5f353cdf8a13cd1dd9499117f71287", "tree_fp": "85e5901717ba7746d406493c34a028b8314068453ac38a6f6bfb3606f10c66a8", "at": "2026-11-05T12:10:51+00:00"}
|
||||||
|
{"task": "task_0003", "cmd": "bash .spec/specs/2026-11-05_blog-5n-admin-deep-optimization/evidence/verify_t3.sh", "exit": 0, "duration_s": 0.15, "stdout_tail_sha256": "409ed685a0bde8695328c75cd1e92fb1e54398e22f911a252c1385f2262db9b3", "tree_fp": "85e5901717ba7746d406493c34a028b8314068453ac38a6f6bfb3606f10c66a8", "at": "2026-11-05T12:10:51+00:00"}
|
||||||
|
{"task": "task_0004", "cmd": "bash .spec/specs/2026-11-05_blog-5n-admin-deep-optimization/evidence/verify_t4.sh", "exit": 0, "duration_s": 0.048, "stdout_tail_sha256": "8338018e17e2e8b7c9c6aeb8468f164497a332d7b9fbe69a027eec2f3eed2dab", "tree_fp": "85e5901717ba7746d406493c34a028b8314068453ac38a6f6bfb3606f10c66a8", "at": "2026-11-05T12:10:51+00:00"}
|
||||||
|
{"task": "task_0005", "cmd": "bash .spec/specs/2026-11-05_blog-5n-admin-deep-optimization/evidence/verify_t5.sh", "exit": 0, "duration_s": 21.431, "stdout_tail_sha256": "99a2c86534e476be7ff3365a14ce028321c3efe5f6849f7831d8c5fe5fc4e6e2", "tree_fp": "85e5901717ba7746d406493c34a028b8314068453ac38a6f6bfb3606f10c66a8", "at": "2026-11-05T12:11:12+00:00"}
|
||||||
|
{"task": "task_0006", "cmd": "bash .spec/specs/2026-11-05_blog-5n-admin-deep-optimization/evidence/verify_t6.sh", "exit": 0, "duration_s": 21.644, "stdout_tail_sha256": "625069f2ee6d8e41eb00af14da68816cbe639979296adc0375ffc20780fadf19", "tree_fp": "85e5901717ba7746d406493c34a028b8314068453ac38a6f6bfb3606f10c66a8", "at": "2026-11-05T12:11:34+00:00"}
|
||||||
|
{"task": "task_0007", "cmd": "bash .spec/specs/2026-11-05_blog-5n-admin-deep-optimization/evidence/verify_t7.sh", "exit": 0, "duration_s": 0.06, "stdout_tail_sha256": "6d54a7fb09f6d6b7db49450f842bd397759f0aede9ed53b6e2edb0aa67271f6b", "tree_fp": "85e5901717ba7746d406493c34a028b8314068453ac38a6f6bfb3606f10c66a8", "at": "2026-11-05T12:11:34+00:00"}
|
||||||
|
{"task": "task_0008", "cmd": "bash .spec/specs/2026-11-05_blog-5n-admin-deep-optimization/evidence/verify_t8.sh", "exit": 0, "duration_s": 0.205, "stdout_tail_sha256": "2f95596c0c69d4a3e9e5e800876d2007624acea62f90b35243c0dc3aa3f737c8", "tree_fp": "85e5901717ba7746d406493c34a028b8314068453ac38a6f6bfb3606f10c66a8", "at": "2026-11-05T12:11:35+00:00"}
|
||||||
|
{"task": "task_0001", "cmd": "bash .spec/specs/2026-11-05_blog-5n-admin-deep-optimization/evidence/verify_t1.sh", "exit": 0, "duration_s": 23.758, "stdout_tail_sha256": "cced3c08cf1601b8177b7728e48c6add6982099d0785fa9fa4dd470b1f4900a5", "tree_fp": "85e5901717ba7746d406493c34a028b8314068453ac38a6f6bfb3606f10c66a8", "at": "2026-11-05T12:12:11+00:00"}
|
||||||
|
{"task": "task_0002", "cmd": "bash .spec/specs/2026-11-05_blog-5n-admin-deep-optimization/evidence/verify_t2.sh", "exit": 0, "duration_s": 0.083, "stdout_tail_sha256": "a539567e2a1bad8a450d90a3998841921d5f353cdf8a13cd1dd9499117f71287", "tree_fp": "85e5901717ba7746d406493c34a028b8314068453ac38a6f6bfb3606f10c66a8", "at": "2026-11-05T12:12:11+00:00"}
|
||||||
|
{"task": "task_0003", "cmd": "bash .spec/specs/2026-11-05_blog-5n-admin-deep-optimization/evidence/verify_t3.sh", "exit": 0, "duration_s": 0.146, "stdout_tail_sha256": "409ed685a0bde8695328c75cd1e92fb1e54398e22f911a252c1385f2262db9b3", "tree_fp": "85e5901717ba7746d406493c34a028b8314068453ac38a6f6bfb3606f10c66a8", "at": "2026-11-05T12:12:11+00:00"}
|
||||||
|
{"task": "task_0004", "cmd": "bash .spec/specs/2026-11-05_blog-5n-admin-deep-optimization/evidence/verify_t4.sh", "exit": 0, "duration_s": 0.043, "stdout_tail_sha256": "8338018e17e2e8b7c9c6aeb8468f164497a332d7b9fbe69a027eec2f3eed2dab", "tree_fp": "85e5901717ba7746d406493c34a028b8314068453ac38a6f6bfb3606f10c66a8", "at": "2026-11-05T12:12:11+00:00"}
|
||||||
|
{"task": "task_0005", "cmd": "bash .spec/specs/2026-11-05_blog-5n-admin-deep-optimization/evidence/verify_t5.sh", "exit": 0, "duration_s": 16.974, "stdout_tail_sha256": "99a2c86534e476be7ff3365a14ce028321c3efe5f6849f7831d8c5fe5fc4e6e2", "tree_fp": "85e5901717ba7746d406493c34a028b8314068453ac38a6f6bfb3606f10c66a8", "at": "2026-11-05T12:12:28+00:00"}
|
||||||
|
{"task": "task_0006", "cmd": "bash .spec/specs/2026-11-05_blog-5n-admin-deep-optimization/evidence/verify_t6.sh", "exit": 0, "duration_s": 28.626, "stdout_tail_sha256": "625069f2ee6d8e41eb00af14da68816cbe639979296adc0375ffc20780fadf19", "tree_fp": "85e5901717ba7746d406493c34a028b8314068453ac38a6f6bfb3606f10c66a8", "at": "2026-11-05T12:12:57+00:00"}
|
||||||
|
{"task": "task_0007", "cmd": "bash .spec/specs/2026-11-05_blog-5n-admin-deep-optimization/evidence/verify_t7.sh", "exit": 0, "duration_s": 0.062, "stdout_tail_sha256": "6d54a7fb09f6d6b7db49450f842bd397759f0aede9ed53b6e2edb0aa67271f6b", "tree_fp": "85e5901717ba7746d406493c34a028b8314068453ac38a6f6bfb3606f10c66a8", "at": "2026-11-05T12:12:57+00:00"}
|
||||||
|
{"task": "task_0008", "cmd": "bash .spec/specs/2026-11-05_blog-5n-admin-deep-optimization/evidence/verify_t8.sh", "exit": 0, "duration_s": 0.172, "stdout_tail_sha256": "2f95596c0c69d4a3e9e5e800876d2007624acea62f90b35243c0dc3aa3f737c8", "tree_fp": "85e5901717ba7746d406493c34a028b8314068453ac38a6f6bfb3606f10c66a8", "at": "2026-11-05T12:12:57+00:00"}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
<!-- spec-format: 2 -->
|
||||||
|
# blog-5n:后台深度优化全家桶(14 项全量)
|
||||||
|
|
||||||
|
- **slug**: 2026-11-05_blog-5n-admin-deep-optimization
|
||||||
|
- **日期**: 2026-11-05
|
||||||
|
- **状态**: 进行中
|
||||||
|
- Git integration branch:`spec/2026-11-05_blog-5n-admin-deep-optimization`
|
||||||
|
- 证据协议:evidence: runs
|
||||||
|
- **提案**: 用户拍板「全部都做包括暂缓的」:写作链路(A1 翻译进构建链/A2 标签防手滑/A3 起手模板/A4 SEO 提示)、发布反馈(B1 部署状态条/B2 favicon+PWA)、内容管理(C1 首页设置分组/C2 内容仪表盘/C3 中英对照提示)、工具入口(D1 速通坞)、安全网(E1 回滚指引)、暂缓档转正(Sveltia 0.252→0.259 升级、暗色预览、媒体库归置)。多队友协同:@worker-one + 4 子代理 + 主会话集成。
|
||||||
|
|
||||||
|
## 1. 问题定义
|
||||||
|
- **项目目标**: 后台从「功能齐」到「全程顺手」——写作(双语零操作/标签不散/起手不空)、发布(状态可见/入口收拢/回滚有指引)、管理(分组/统计/对照)三条线一步到位;底层 Sveltia 升级到 0.259.0 并全量回归。
|
||||||
|
- **目标用户**: 站长(唯一编辑者,高频写作与内容管理)
|
||||||
|
- **核心价值**: 把这一周散落的「后台不好用」类反馈一次性清零;英文站从手动操作变全自动。
|
||||||
|
|
||||||
|
## 2. 假设与待确认
|
||||||
|
|
||||||
|
### 2.1 已确认事实
|
||||||
|
- TMT 翻译脚本 translate_posts.py 幂等 + locked 保护,可直接挂构建链
|
||||||
|
- Sveltia 最新 0.259.0(registry 核实);本站 bundle 0.252.0 pinned 本地
|
||||||
|
- Artalk 评论管理在 https://wait4x.online/artalk;rollback 脚本在 infra/deploy/ 已存在
|
||||||
|
- build.sh 为部署链核心(A1/B1/C2 三项都挂它,由主会话统一改避免竞态)
|
||||||
|
|
||||||
|
### 2.2 关键假设
|
||||||
|
- Sveltia 0.259 与 0.252 对本站用到的 API(registerEditorComponent/registerPreviewStyle/config schema)兼容(升级后全量回归验证)
|
||||||
|
- TMT 翻译单篇耗时 < 构建容忍度(守卫超时 180s,失败不阻断部署、保留旧英文版)
|
||||||
|
|
||||||
|
### 2.3 待确认问题
|
||||||
|
- [x] 全量范围:用户明确「全部都做包括暂缓的」
|
||||||
|
- [x] 协同分工:@worker-one 独立文件 ×2、子代理 ×4(新文件/独立文件)、共享文件(config.yml/index.html/build.sh)主会话独占
|
||||||
|
- [x] 媒体库归置取低风险形态:照片目录不动(摄取管线契约),归置=上传落点核对+文档+hints
|
||||||
|
|
||||||
|
### 2.4 可选解释与取舍
|
||||||
|
- A2 标签防手滑:若 Sveltia select 无 multiple 则退 list+常用标签 hint(不引自定义 widget)
|
||||||
|
- A3 起手模板:利用现有隐藏「写作模板」+ Sveltia 条目复制能力 + hint 指引(不建模板机制)
|
||||||
|
- C1 分组:仅改 home 单例形状(schema+yaml+消费端+后台表单),不动其他单例
|
||||||
|
|
||||||
|
### 2.5 已证伪路径
|
||||||
|
- 无(新增包)
|
||||||
|
|
||||||
|
## 3. 功能范围
|
||||||
|
|
||||||
|
### 3.1 核心功能(MVP)
|
||||||
|
- [x] T1 Sveltia 0.259 升级 + 全量回归(三脚本注册/预览/集合加载)
|
||||||
|
- [x] T2 A1 翻译进构建链(translate-step 守卫 + build.sh 挂载)
|
||||||
|
- [x] T3 A2+A3+A4+C3 表单优化批(config.yml:标签建议/模板指引/SEO 提示/中英对照)
|
||||||
|
- [x] T4 B1+B2+D1+E1 后台页面批(状态条/favicon/PWA/速通坞/回滚指引)
|
||||||
|
- [x] T5 C1 首页设置分组(schema 迁移 + 消费端 + 表单)
|
||||||
|
- [x] T6 C2 内容仪表盘(构建期统计 + 静态页)
|
||||||
|
- [x] T7 暗色预览(preview-style.js 双主题)
|
||||||
|
- [x] T8 媒体库归置(落点核对 + hints + 文档)
|
||||||
|
|
||||||
|
### 3.2 扩展功能
|
||||||
|
- 无
|
||||||
|
|
||||||
|
### 3.3 不在范围内
|
||||||
|
- 照片摄取管线目录迁移;TMT 凭据变更;评论系统本身
|
||||||
|
|
||||||
|
## 4. 最小实现路径
|
||||||
|
- 最简可行:新文件优先(守卫脚本/状态条/统计脚本/manifest)+ 共享文件主会话三批统一改(build.sh/config.yml/index.html)+ 升级先行(后续工作全部在新版上验证)
|
||||||
|
- 暂不引入: 自定义 Sveltia widget 开发、翻译并发加速
|
||||||
|
- 不做的抽象: 后台微前端/工具页框架(静态页够用)
|
||||||
|
|
||||||
|
### 5.1 验证策略
|
||||||
|
- 范围级别:package
|
||||||
|
- 变更对象: public/admin 全域 + build.sh + content/site schema + 文档
|
||||||
|
- 快速检查: `pnpm astro build` + `python3 scripts/build-perf/config-check.py .`
|
||||||
|
- 集成检查: E2E 61/61 + content-lint 0/0/0 + admin 页加载零控制台错误(playwright 预登录态)+ build-status.json 产物断言 + 翻译守卫 dry-run
|
||||||
|
- 全项目检查: 部署后人工核验后台(登录态表单可编辑/状态条/速通坞/暗色预览);**升级条件**——仅当 Sveltia 升级引致行为差异时升级全站回归,本包已含升级后 E2E 全跑
|
||||||
|
- 升级触发: admin 为独立资源面,升级为「后台人工核验」即可
|
||||||
|
|
||||||
|
## 6. 成功标准与验证方式
|
||||||
|
- [x] Sveltia 0.259 上线且三脚本/预览/表单全可用 -> verify: task_0001
|
||||||
|
- [x] 中文保存→英文自动出(守卫不阻断)-> verify: task_0002
|
||||||
|
- [x] 表单四优化与页面四件套生效 -> verify: task_0003 + task_0004
|
||||||
|
- [x] 首页分组/仪表盘/暗色预览/媒体归置落地 -> verify: task_0005..task_0008
|
||||||
|
|
||||||
|
## 7. 关联与风险
|
||||||
|
- 最大风险 = Sveltia 升级破坏现有定制(三脚本 API 面);缓解:升级后立即 playwright 冒烟 + 生产灰度观察,异常即回滚 bundle(git revert 单文件)
|
||||||
|
- 翻译挂链风险 = TMT 配额/网络抖动;缓解:守卫永不阻断 + last-good 英文版保留
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
# Phase 5n:后台深度优化全家桶 - 任务拆解
|
||||||
|
|
||||||
|
## 使用规则
|
||||||
|
- 共享文件(public/admin/config.yml、public/admin/index.html、infra/deploy/build.sh)仅主会话编辑,队友产出片段/新文件
|
||||||
|
- E2E 口径:`pnpm astro build → gz-server.py 18925 <dist> → E2E_BASE=http://127.0.0.1:18925 node tests/e2e.mjs`(基线 61)
|
||||||
|
|
||||||
|
## 阶段一:实现
|
||||||
|
|
||||||
|
- [x] task_0001 Sveltia 0.259 升级与回归
|
||||||
|
- claim: orchestrator
|
||||||
|
- boundary: public/admin/sveltia-cms.js(bundle 置换)
|
||||||
|
- verify: `bash .spec/specs/2026-11-05_blog-5n-admin-deep-optimization/evidence/verify_t1.sh`
|
||||||
|
|
||||||
|
- [x] task_0002 A1 翻译进构建链
|
||||||
|
- claim: orchestrator + worker-one
|
||||||
|
- boundary: scripts/deploy/translate-step.sh(新,worker-one)、infra/deploy/build.sh(主会话挂载)
|
||||||
|
- verify: `bash .spec/specs/2026-11-05_blog-5n-admin-deep-optimization/evidence/verify_t2.sh`
|
||||||
|
|
||||||
|
- [x] task_0003 A2+A3+A4+C3 表单优化批
|
||||||
|
- claim: orchestrator
|
||||||
|
- boundary: public/admin/config.yml(标签/模板指引/SEO/中英 hint)、content/posts/writing-template.md
|
||||||
|
- verify: `bash .spec/specs/2026-11-05_blog-5n-admin-deep-optimization/evidence/verify_t3.sh`
|
||||||
|
|
||||||
|
- [x] task_0004 B1+B2+D1+E1 后台页面批
|
||||||
|
- claim: orchestrator + worker-one + 子代理
|
||||||
|
- boundary: public/admin/deploy-status.js(新,子代理)、public/admin/manifest.webmanifest(新,子代理)、docs/后台回滚指引.md(新,worker-one)、public/admin/index.html(主会话接线)
|
||||||
|
- verify: `bash .spec/specs/2026-11-05_blog-5n-admin-deep-optimization/evidence/verify_t4.sh`
|
||||||
|
|
||||||
|
- [x] task_0005 C1 首页设置分组
|
||||||
|
- claim: orchestrator + 子代理
|
||||||
|
- boundary: src/content.config.ts(siteHome schema)、content/site/home.yaml、src/lib/site-content.ts(消费端)、public/admin/config.yml(home 条目,主会话拼装子代理片段)
|
||||||
|
- verify: `bash .spec/specs/2026-11-05_blog-5n-admin-deep-optimization/evidence/verify_t5.sh`
|
||||||
|
|
||||||
|
- [x] task_0006 C2 内容仪表盘
|
||||||
|
- claim: 子代理
|
||||||
|
- boundary: scripts/build-perf/gen-admin-stats.mjs(新)、infra/deploy/build.sh 挂载(主会话)
|
||||||
|
- verify: `bash .spec/specs/2026-11-05_blog-5n-admin-deep-optimization/evidence/verify_t6.sh`
|
||||||
|
|
||||||
|
- [x] task_0007 暗色预览
|
||||||
|
- claim: 子代理
|
||||||
|
- boundary: public/admin/preview-style.js(双主题改造)
|
||||||
|
- verify: `bash .spec/specs/2026-11-05_blog-5n-admin-deep-optimization/evidence/verify_t7.sh`
|
||||||
|
|
||||||
|
- [x] task_0008 媒体库归置
|
||||||
|
- claim: orchestrator
|
||||||
|
- boundary: public/admin/config.yml(media_folder hints)、docs/管理台.md
|
||||||
|
- verify: `bash .spec/specs/2026-11-05_blog-5n-admin-deep-optimization/evidence/verify_t8.sh`
|
||||||
@@ -3,6 +3,7 @@ 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 { readdirSync, readFileSync } from 'node:fs';
|
||||||
import rehypeLinkCards, { linkCardsReport } from './src/lib/rehype-link-cards';
|
import rehypeLinkCards, { linkCardsReport } from './src/lib/rehype-link-cards';
|
||||||
import rehypeClassWhitelist from './src/lib/rehype-class-whitelist';
|
import rehypeClassWhitelist from './src/lib/rehype-class-whitelist';
|
||||||
import rehypeRaw from 'rehype-raw';
|
import rehypeRaw from 'rehype-raw';
|
||||||
@@ -18,9 +19,38 @@ const date =
|
|||||||
process.env.GITHUB_COMMIT_DATE ||
|
process.env.GITHUB_COMMIT_DATE ||
|
||||||
(() => { try { return execSync('git log -1 --format=%cI').toString().trim(); } catch { return new Date().toISOString(); } })();
|
(() => { try { return execSync('git log -1 --format=%cI').toString().trim(); } catch { return new Date().toISOString(); } })();
|
||||||
|
|
||||||
|
// 隐藏文章不进 sitemap(内容侧契约见 src/lib/post-visibility.ts)。
|
||||||
|
// 这里在配置期直接扫 frontmatter:Astro 的 content collections 在配置上下文不可用,
|
||||||
|
// 而 sitemap 的 filter 只拿到 URL。判定两条通道:`hidden: true` 与 tags 里的 hidden。
|
||||||
|
const hiddenSlugs = (() => {
|
||||||
|
const set = new Set();
|
||||||
|
try {
|
||||||
|
const dir = 'content/posts';
|
||||||
|
for (const file of readdirSync(dir)) {
|
||||||
|
if (!file.endsWith('.md')) continue;
|
||||||
|
const raw = readFileSync(`${dir}/${file}`, 'utf8');
|
||||||
|
const head = (raw.split('\n---\n')[0] ?? '').replace(/^---\n/, '');
|
||||||
|
if (/^hidden:\s*true\s*$/m.test(head)) { set.add(file.replace(/\.md$/, '')); continue; }
|
||||||
|
// tags 可能是行内数组,也可能是多行列表:抓出 tags 段再找 hidden 词
|
||||||
|
const m = /^tags:[ \t]*([\s\S]*?)(?=^\w|\s*$)/m.exec(head);
|
||||||
|
const tagBlock = m ? m[1] : '';
|
||||||
|
if (/\bhidden\b/.test(tagBlock)) set.add(file.replace(/\.md$/, ''));
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
/* 内容目录不可读时宁可不排除,也不让构建挂掉 */
|
||||||
|
}
|
||||||
|
return [...set];
|
||||||
|
})();
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
site: 'https://wait4x.online',
|
site: 'https://wait4x.online',
|
||||||
integrations: [pagefind(), sitemap(), linkCardsReport()],
|
integrations: [
|
||||||
|
pagefind(),
|
||||||
|
sitemap({
|
||||||
|
filter: (page) => !hiddenSlugs.some((slug) => page.includes(`/archives/${slug}/`)),
|
||||||
|
}),
|
||||||
|
linkCardsReport(),
|
||||||
|
],
|
||||||
markdown: {
|
markdown: {
|
||||||
// 独立成段的裸链接 → OG 预览卡(构建期抓取,失败回落普通链接;见 src/lib/rehype-link-cards.ts)
|
// 独立成段的裸链接 → OG 预览卡(构建期抓取,失败回落普通链接;见 src/lib/rehype-link-cards.ts)
|
||||||
// 顺序:rehypeRaw 先把 markdown 里的 raw HTML 解析成 hast element(否则白名单看不到 span),
|
// 顺序:rehypeRaw 先把 markdown 里的 raw HTML 解析成 hast element(否则白名单看不到 span),
|
||||||
|
|||||||
@@ -27,5 +27,12 @@
|
|||||||
"image": "https://astro.build/og/astro.jpg",
|
"image": "https://astro.build/og/astro.jpg",
|
||||||
"favicon": "https://astro.build/favicon.svg",
|
"favicon": "https://astro.build/favicon.svg",
|
||||||
"ts": 1793593759816
|
"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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
---
|
---
|
||||||
pubDate: 2026-04-29
|
pubDate: 2026-04-29
|
||||||
tags: []
|
text: '@wait4x'
|
||||||
text: "@wait4x"
|
tags:
|
||||||
|
- me
|
||||||
|
pinned: true
|
||||||
|
hidden: false
|
||||||
---
|
---
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
---
|
||||||
|
pubDate: 2026-11-02
|
||||||
|
text: |-
|
||||||
|
瞬间现在和文章同级表达力:`text` 支持 markdown 与排版类,<span class="c-teal font-kai">这一句是青色 + 楷体</span>,也可以**加粗**。
|
||||||
|
|
||||||
|
另外每条瞬间自带评论区——展开才加载,不刷时间线就不为它付成本。
|
||||||
|
tags:
|
||||||
|
- 站点更新
|
||||||
|
pinned: false
|
||||||
|
hidden: true
|
||||||
|
---
|
||||||
|
|
||||||
|
站点更新记录:瞬间富文本与逐条评论上线。
|
||||||
|
After Width: | Height: | Size: 19 MiB |
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"title": "夜色金茂湾",
|
||||||
|
"image": "1000039852_0_1_20397409.jpg",
|
||||||
|
"group": "landscape",
|
||||||
|
"caption": "小船儿飘呀飘"
|
||||||
|
}
|
||||||
@@ -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 | `` | 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:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 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.
|
||||||
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 210 KiB |
|
After Width: | Height: | Size: 772 KiB |
|
After Width: | Height: | Size: 2.8 MiB |
|
After Width: | Height: | Size: 1.8 MiB |
@@ -1,11 +1,16 @@
|
|||||||
---
|
---
|
||||||
title: "等待戈多"
|
title: 等待戈多
|
||||||
pubDate: 2026-03-31
|
pubDate: 2026-03-31
|
||||||
category: reading
|
category: reading
|
||||||
tags: [read]
|
tags:
|
||||||
|
- read
|
||||||
|
pinned: false
|
||||||
mood: '#b45309'
|
mood: '#b45309'
|
||||||
bookAuthor: '萨缪尔·贝克特'
|
bookAuthor: 萨缪尔·贝克特
|
||||||
description: '无望之待里,陪伴是囚笼唯一的呼吸缝隙。'
|
description: 无望之待里,陪伴是囚笼唯一的呼吸缝隙。
|
||||||
|
cover: ./assets/等待戈多.png
|
||||||
|
hidden: false
|
||||||
|
draft: false
|
||||||
---
|
---
|
||||||
|
|
||||||
### 谁是戈多?为何等待?
|
### 谁是戈多?为何等待?
|
||||||
|
|||||||
@@ -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: '面纱之下:理想的殉道者与真实的幸存者。'
|
|||||||
------------------------------------------------------------------------
|
------------------------------------------------------------------------
|
||||||
|
|
||||||
- 我不了解你,我一点儿都不了解你。
|
- 我不了解你,我一点儿都不了解你。
|
||||||
|
|
||||||
- 我知道你愚蠢、轻浮、没有头脑,但是我爱你。我知道你的目标和理想既庸俗又普通,但是我爱你。我知道你是二流货色,但是我爱你。想一想真是好笑,我竭力去喜欢那些讨你喜欢的东西,忍受折磨也要对你隐瞒起自己,实际上我并不无知粗俗、不爱散播丑闻也不愚蠢。我知道你何等害怕智慧,便尽我所能让你觉得我是个大傻瓜,跟你认识的其他人一样。我知道你嫁给我只图一时利益,我是那样爱你,我不在乎。大多数人,就我所知,当他们爱一个人,却没有得到爱的回报时就会觉得委屈不平,甚至愈发愤怒和痛苦。我不是那样,我从来没有指望你爱我,我看不出任何理由让你爱我,我也从来没想过自己会被人爱。我很感激能被允许爱你,当我时常想起你高兴跟我在一起,或者当我发现你眼中闪烁着愉快的爱意时,我就会欣喜若狂。我尽量不让我的爱来烦扰你,我知道那会让我承受不起,所以我一直察言观色,留意我的爱让你厌烦的最初迹象。大部分丈夫认为那是一种权力,我却准备当成恩惠来接受。
|
- 我知道你愚蠢、轻浮、没有头脑,但是我爱你。我知道你的目标和理想既庸俗又普通,但是我爱你。我知道你是二流货色,但是我爱你。想一想真是好笑,我竭力去喜欢那些讨你喜欢的东西,忍受折磨也要对你隐瞒起自己,实际上我并不无知粗俗、不爱散播丑闻也不愚蠢。我知道你何等害怕智慧,便尽我所能让你觉得我是个大傻瓜,跟你认识的其他人一样。我知道你嫁给我只图一时利益,我是那样爱你,我不在乎。大多数人,就我所知,当他们爱一个人,却没有得到爱的回报时就会觉得委屈不平,甚至愈发愤怒和痛苦。我不是那样,我从来没有指望你爱我,我看不出任何理由让你爱我,我也从来没想过自己会被人爱。我很感激能被允许爱你,当我时常想起你高兴跟我在一起,或者当我发现你眼中闪烁着愉快的爱意时,我就会欣喜若狂。我尽量不让我的爱来烦扰你,我知道那会让我承受不起,所以我一直察言观色,留意我的爱让你厌烦的最初迹象。大部分丈夫认为那是一种权力,我却准备当成恩惠来接受。
|
||||||
|
|
||||||
- 他知道你虚荣、懦弱、自私自利,他想让我亲眼看到这一切。他知道你会像野兔一样,危险一来就跑得远远的。他知道我深受蒙骗才会认为你爱上了我,因为他知道你不会爱任何人,只爱你自己。他知道你会牺牲我,好让自己毫发无损地逃脱出去。
|
- 他知道你虚荣、懦弱、自私自利,他想让我亲眼看到这一切。他知道你会像野兔一样,危险一来就跑得远远的。他知道我深受蒙骗才会认为你爱上了我,因为他知道你不会爱任何人,只爱你自己。他知道你会牺牲我,好让自己毫发无损地逃脱出去。
|
||||||
|
|
||||||
- 死亡既突然又无情,在一条条曲折的街巷上匆匆穿过。
|
- 死亡既突然又无情,在一条条曲折的街巷上匆匆穿过。
|
||||||
|
|
||||||
- 黎明乍现,阳光触到雾霭,令其闪闪发白,犹如雪之幽灵降至即将熄灭的星宿之上。
|
- 黎明乍现,阳光触到雾霭,令其闪闪发白,犹如雪之幽灵降至即将熄灭的星宿之上。
|
||||||
|
|
||||||
- 你瞧,这里只有你跟我踏踏实实在坚实的地上行走。修女们走在天上,而你丈夫则走在黑暗里。
|
- 你瞧,这里只有你跟我踏踏实实在坚实的地上行走。修女们走在天上,而你丈夫则走在黑暗里。
|
||||||
|
|
||||||
- 他的灵魂撕裂了,他一直活在一种虚假的构想之中,当真相击碎了幻象,他便认为现实本身也被击碎了。
|
- 他的灵魂撕裂了,他一直活在一种虚假的构想之中,当真相击碎了幻象,他便认为现实本身也被击碎了。
|
||||||
|
|
||||||
- 这个时辰的城市街巷空空荡荡,比任何时候都更像一座死城。路人一个个神色迷离,不禁让人怀疑他们全都是鬼魂。天空晴朗无云,初升的太阳将圣洁和煦的光芒洒满大地。很难想象,在这个愉悦、清新、爽朗的黎明,这座城市却像被疯子一手扼住咽喉的人,在瘟疫的黑暗掌控下苟延残喘。不可思议的是,当人类在痛苦挣扎、在恐惧中走向死亡时,大自然(蓝色的天空如孩童的心一般清澈)竟会如此无动于衷。
|
- 这个时辰的城市街巷空空荡荡,比任何时候都更像一座死城。路人一个个神色迷离,不禁让人怀疑他们全都是鬼魂。天空晴朗无云,初升的太阳将圣洁和煦的光芒洒满大地。很难想象,在这个愉悦、清新、爽朗的黎明,这座城市却像被疯子一手扼住咽喉的人,在瘟疫的黑暗掌控下苟延残喘。不可思议的是,当人类在痛苦挣扎、在恐惧中走向死亡时,大自然(蓝色的天空如孩童的心一般清澈)竟会如此无动于衷。
|
||||||
|
|
||||||
- 安宁只存在于人的灵魂中。
|
- 安宁只存在于人的灵魂中。
|
||||||
|
|
||||||
- 所有的人,乃至整个人类,就如同这条河里的水滴一样,流淌不定,一滴滴彼此接近,却又相距遥远,汇成一股无名的巨流奔向大海。既然一切转瞬即逝,任何事物都无关宏旨,人们竟还要荒唐地看重那些微不足道的事情,让自己也让别人遭受不幸,这实在太可悲了。
|
- 所有的人,乃至整个人类,就如同这条河里的水滴一样,流淌不定,一滴滴彼此接近,却又相距遥远,汇成一股无名的巨流奔向大海。既然一切转瞬即逝,任何事物都无关宏旨,人们竟还要荒唐地看重那些微不足道的事情,让自己也让别人遭受不幸,这实在太可悲了。
|
||||||
|
|
||||||
- 小小的两滴水随着河流默默流向未知,这两滴水相对而言是那样独特,但在旁观者的眼里,不过是河流中无法辨识的组成部分。
|
- 小小的两滴水随着河流默默流向未知,这两滴水相对而言是那样独特,但在旁观者的眼里,不过是河流中无法辨识的组成部分。
|
||||||
|
|
||||||
- 我又明白什么呢,生命是那么奇特。我觉得自己就像个一辈子都住在小池塘边上的人,突然间看见了大海,让我有点喘不过气来,但心里又充满了喜悦。我不想死,想活下去,于是感到一股新的勇气。我就好像那些老水手,起航驶向尚未发现的海洋,我的灵魂渴求未知的一切。
|
- 我又明白什么呢,生命是那么奇特。我觉得自己就像个一辈子都住在小池塘边上的人,突然间看见了大海,让我有点喘不过气来,但心里又充满了喜悦。我不想死,想活下去,于是感到一股新的勇气。我就好像那些老水手,起航驶向尚未发现的海洋,我的灵魂渴求未知的一切。
|
||||||
|
|
||||||
- 小小的两滴水默默地、默默地流向那黑暗、永恒的大海。
|
- 小小的两滴水默默地、默默地流向那黑暗、永恒的大海。
|
||||||
|
|
||||||
- 道。我们有些人在鸦片中寻找道,有些人在上帝那儿,有些人寻求威士忌,或去爱里寻找。这道终归只有一条,可它不通向任何地方。
|
- 道。我们有些人在鸦片中寻找道,有些人在上帝那儿,有些人寻求威士忌,或去爱里寻找。这道终归只有一条,可它不通向任何地方。
|
||||||
|
|
||||||
- 这一切既新奇又感人,她的心灵犹如一片平静而灰暗的大海,波浪起伏,蔚然壮观,阴郁浩瀚令人畏惧,突然间,一缕阳光投射过来,让一切变得活泼、友好而快乐。
|
- 这一切既新奇又感人,她的心灵犹如一片平静而灰暗的大海,波浪起伏,蔚然壮观,阴郁浩瀚令人畏惧,突然间,一缕阳光投射过来,让一切变得活泼、友好而快乐。
|
||||||
|
|
||||||
- 人生何其短暂,世界本来就充满了苦痛,人们却还要折磨自己,这岂不太可怜了吗?
|
- 人生何其短暂,世界本来就充满了苦痛,人们却还要折磨自己,这岂不太可怜了吗?
|
||||||
|
|
||||||
- 她的性格就像一片乡野,初见时感觉辽阔而冷漠,但不久就会在巍峨山岭的褶皱间发现一个个掩映在果树丛中的欢腾的村庄,看见郁郁葱葱的草地上欢快流淌的一条条小河。这番宜人的景象会让你惊奇,甚至熨帖安心,但远处狂风劲吹的黄褐色高地尚不足以使你感到自如自在。
|
- 她的性格就像一片乡野,初见时感觉辽阔而冷漠,但不久就会在巍峨山岭的褶皱间发现一个个掩映在果树丛中的欢腾的村庄,看见郁郁葱葱的草地上欢快流淌的一条条小河。这番宜人的景象会让你惊奇,甚至熨帖安心,但远处狂风劲吹的黄褐色高地尚不足以使你感到自如自在。
|
||||||
|
|
||||||
- 只有一种办法赢得人心,那就是让自己成为人们会去爱的人。
|
- 只有一种办法赢得人心,那就是让自己成为人们会去爱的人。
|
||||||
|
|
||||||
- 我说不清。我不知道她们追求幻觉这一点是否真的那么重要,还是这种生活本身就很美好。我有个想法,唯一让我们有可能不带嫌恶地关注我们所生活的这个世界的,就是人类不断从混沌中创造出的美。他们描绘的画,他们谱出的乐曲,他们写的书,还有他们的生活。这一切中,最富于美的就是美好的生活,那是件完美的艺术品。
|
- 我说不清。我不知道她们追求幻觉这一点是否真的那么重要,还是这种生活本身就很美好。我有个想法,唯一让我们有可能不带嫌恶地关注我们所生活的这个世界的,就是人类不断从混沌中创造出的美。他们描绘的画,他们谱出的乐曲,他们写的书,还有他们的生活。这一切中,最富于美的就是美好的生活,那是件完美的艺术品。
|
||||||
|
|
||||||
- 道就是道路和行道者。那是一条永恒的路,所有的生命存在行走其上,但它并非由生命存在所创造,因为它本身便是生命存在。它什么都是,又什么都不是。万物由道所生,与道相符,最后万物又回归于道。它是一块方形却无四角,是种声音却不被聆听,是幅图画却未有形状;它是一张巨大的网,网眼阔如海洋,却什么都无法穿过;它是万物寻求庇护的避难所;它无处可寻,但你‘不窥牖’便可‘见天道’;它要人学会欲无所欲,让一切顺其自然。谦卑者尽得保全,屈身者终将直立。‘祸兮,福之所依;福兮,祸之所伏’;但谁能说清什么时候会出现转折点?追求柔慈之人会如小孩子一样平和。柔慈为进攻者获取胜利,为守卫者求得保全。战胜自己的人最为强大。
|
- 道就是道路和行道者。那是一条永恒的路,所有的生命存在行走其上,但它并非由生命存在所创造,因为它本身便是生命存在。它什么都是,又什么都不是。万物由道所生,与道相符,最后万物又回归于道。它是一块方形却无四角,是种声音却不被聆听,是幅图画却未有形状;它是一张巨大的网,网眼阔如海洋,却什么都无法穿过;它是万物寻求庇护的避难所;它无处可寻,但你‘不窥牖’便可‘见天道’;它要人学会欲无所欲,让一切顺其自然。谦卑者尽得保全,屈身者终将直立。‘祸兮,福之所依;福兮,祸之所伏’;但谁能说清什么时候会出现转折点?追求柔慈之人会如小孩子一样平和。柔慈为进攻者获取胜利,为守卫者求得保全。战胜自己的人最为强大。
|
||||||
|
|
||||||
- 自由!这便是一直萦绕在她心中的念头。尽管未来仍旧模糊,这一念头却像河上的薄雾,在清晨阳光的照耀下焕发虹彩。自由!不仅仅是挣脱烦恼的束缚,解除那让她消沉的伴侣关系的自由。自由!不仅是逃离死亡威胁的自由,更是逃离让她降低人格的爱情,逃脱所有精神束缚的自由,一种抽离出肉体的精神的自由。与自由相伴的,还有勇气,以及无论发生什么都毫不在乎的坚强气质。
|
- 自由!这便是一直萦绕在她心中的念头。尽管未来仍旧模糊,这一念头却像河上的薄雾,在清晨阳光的照耀下焕发虹彩。自由!不仅仅是挣脱烦恼的束缚,解除那让她消沉的伴侣关系的自由。自由!不仅是逃离死亡威胁的自由,更是逃离让她降低人格的爱情,逃脱所有精神束缚的自由,一种抽离出肉体的精神的自由。与自由相伴的,还有勇气,以及无论发生什么都毫不在乎的坚强气质。
|
||||||
|
|
||||||
- 她以为自己变了,以为自己意志坚强,会以一个冷静自持的女人的面目回到香港。心中掠过一个个崭新的念头,就像阳光下翻飞的黄色蝴蝶,让她对美好的未来充满期望。自由就像光的精灵召唤她,整个世界犹如一片广袤的平原,任她迈着轻快的步子昂首前行。她原以为自己已摆脱了肉欲和卑劣的情爱,足以过上一种纯洁健康的精神生活。她曾将自己比作黄昏时分悠然飞跃稻田的白鹭,它们就像安闲自处的头脑中翩然翱翔的片片思绪,可实际上她却仍是欲望的奴隶。
|
- 她以为自己变了,以为自己意志坚强,会以一个冷静自持的女人的面目回到香港。心中掠过一个个崭新的念头,就像阳光下翻飞的黄色蝴蝶,让她对美好的未来充满期望。自由就像光的精灵召唤她,整个世界犹如一片广袤的平原,任她迈着轻快的步子昂首前行。她原以为自己已摆脱了肉欲和卑劣的情爱,足以过上一种纯洁健康的精神生活。她曾将自己比作黄昏时分悠然飞跃稻田的白鹭,它们就像安闲自处的头脑中翩然翱翔的片片思绪,可实际上她却仍是欲望的奴隶。
|
||||||
|
|
||||||
- 我有希望,也有勇气。
|
- 我有希望,也有勇气。
|
||||||
|
|
||||||
- 你用面纱遮蔽他人的窥探,自己也难以一睹世界之实。
|
- 你用面纱遮蔽他人的窥探,自己也难以一睹世界之实。
|
||||||
|
|
||||||
------------------------------------------------------------------------
|
------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -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
|
||||||
---
|
---
|
||||||
|
|
||||||
### 迷失在黑夜中时,不妨抬头看看星空;如果不知道该相信什么,人应当面对自己的良知。
|
### 迷失在黑夜中时,不妨抬头看看星空;如果不知道该相信什么,人应当面对自己的良知。
|
||||||
|
|||||||
@@ -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
|
||||||
---
|
---
|
||||||
|
|
||||||
### 你要回来,我会等你。
|
### 你要回来,我会等你。
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
---
|
||||||
|
title: 写作模板:这篇文章本身就是模板
|
||||||
|
pubDate: 2026-11-02
|
||||||
|
category: tech
|
||||||
|
tags:
|
||||||
|
- 写作
|
||||||
|
- 模板
|
||||||
|
pinned: true
|
||||||
|
description: 「复制起手」母版(后台可见、前台隐藏):演示全站排版能力——标题层级、彩色强调、字族、引用、代码窗、表格、图、链接卡、折叠。新文章从它 Duplicate 开始。
|
||||||
|
hidden: true
|
||||||
|
draft: false
|
||||||
|
hiddenTag: true
|
||||||
|
---
|
||||||
|
|
||||||
|
这篇不是教程,是**可复制的模板**。下面每一节都在真跑——你看到的效果,就是 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">` | 整页换会拉字体体积 |
|
||||||
|
| 图片 | `` | 体积纪律见写作指南 |
|
||||||
|
| 视频 | frontmatter `video:` | 放 public/media,≤50MB |
|
||||||
|
| 链接卡片 | 裸链接单独成段 | 构建期抓 OG,失败回落普通链接 |
|
||||||
|
|
||||||
|
## 七、图片
|
||||||
|
|
||||||
|
图片用相对路径引用,挂在 `content/posts/assets/<slug>/` 下,点击可放大(矢量图不糊):
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 八、链接卡片
|
||||||
|
|
||||||
|
一个裸链接单独成段,构建期会抓取 OG 预览生成卡片;抓不到就退回普通链接,不会坏版:
|
||||||
|
|
||||||
|
https://astro.build
|
||||||
|
|
||||||
|
## 九、折叠长内容
|
||||||
|
|
||||||
|
长附录、完整日志、大段配置可以折叠,零 JS,原生 `details`:
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>展开看一个被折叠的长例子</summary>
|
||||||
|
|
||||||
|
折叠区里可以放任何东西——段落、列表、代码块。
|
||||||
|
|
||||||
|
- 适合放「按需查看」的细节
|
||||||
|
- 不适合放结论(读者可能永远不展开)
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## 十、分隔线之后收尾
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
收尾三句就够:这篇讲了什么、读者该带走什么、下一步去哪。模板不是要你写满十节,而是**在你需要表达时,能力都在手边**。
|
||||||
|
|
||||||
|
下一篇开始,复制本文件,删到只剩你要说的。
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
---
|
||||||
|
title: 佩德罗·巴拉莫
|
||||||
|
pubDate: 2026-04-25
|
||||||
|
category: reading
|
||||||
|
tags:
|
||||||
|
- read
|
||||||
|
pinned: false
|
||||||
|
mood: '#3bfe04'
|
||||||
|
bookAuthor: 胡安·鲁尔福
|
||||||
|
description: 我来科马拉是因为有人对我说,我父亲住在这里,他名叫佩德罗·巴拉莫。
|
||||||
|
cover: ./assets/pasted-image-1793843231509.png
|
||||||
|
hidden: false
|
||||||
|
draft: false
|
||||||
|
---
|
||||||
|
|
||||||
|
### **在回声与亡魂中拼凑荒原**
|
||||||
|
|
||||||
|
“我来科马拉是因为有人对我说,我父亲住在这里,他名叫佩德罗·巴拉莫。”
|
||||||
|
|
||||||
|
就像读刘楚昕的《泥潭》一样,在初读时、在没有看完整本书时,还不知道讲的是怎样的一个故事,鲁尔福很聪明,用别人的话语和多个角度叙述一个故事,写就了这部关于寻父与复仇的小说。他就像是个革命者,开创了这种叙事的先河。在生与死之间,在过去和现在之间,界限变得模糊,在科马拉这座“鬼城”,一个暴君、一个主宰、一个狡猾的智者、一个含情脉脉的男子就这么血淋淋的展现在我的面前,将那个荒芜的时代从现在、过去中拼凑出来。
|
||||||
|
|
||||||
|
#### 旅人与回声:寻父、寻根、埋骨地
|
||||||
|
|
||||||
|
胡安·普雷西亚多带着母亲的遗愿踏入科马拉,表面上是为了讨回父亲欠他们的债,但在心底,这更是一场寻找自我身份的寻根之旅。他是一个被动、纯真甚至有些迷茫的旅人,对父亲佩德罗·巴拉莫的认知仅仅停留在一个名字代号上。他厌恶这个缺席的父亲,却又无法遏制地渴望拼凑出自己生命的来处。 然而,胡安注定无法成为一个传统的复仇者或讨债人。鲁尔福将他塑造为一个‘空心’的倾听者、一个收集回声的容器。正因为他没有强烈的自我,他才能接纳科马拉那些无处不在的低语。这是寻父的旅程,也是奔向死亡的旅程;这是来路也是归途,是寻根之旅亦是埋根之地。当他踏上科马拉的土地,死亡就已经向他慢慢靠近。最终,他被恐惧和亡魂的声浪淹没,安静地在这里离去。胡安用自己的死亡,揭开了这座鬼城的诡异,完成了叙事接力棒的交接,拉开了重塑佩德罗的序幕,更掀开了那个曾经荒芜的时代。
|
||||||
|
|
||||||
|
#### **群像与控诉:血淋淋的暴君拼图**
|
||||||
|
|
||||||
|
小说的精妙之处,首先在于叙事视角的滑动与主线的“退场”。前半部,读者以为这是一个线性的寻父故事,胡安作为第一人称的“我”,是引领我们进入科马拉的向导。然而,当读到中段,读者会猛然惊觉:这个寻父的儿子早已死在街头,他不过是一个“收集回声的容器”。随着胡安的死亡,单线叙事彻底断裂,视角的接力棒交给了村庄里的旁观者与亡灵。通过私生子、管家、神父、受害村民甚至他一生挚爱苏萨娜的喃喃自语,佩德罗·巴拉莫这个原本隐藏在幕后的核心人物,才像拼图一样被一块块拼凑完整。这种从“单线”到“群像”的转换,让佩德罗不再是一个扁平的恶棍,而是一个被多重视角立体雕刻的复杂存在。我们看到了他的残暴与贪婪,也看到了他的孤独与绝望。 为什么一定要用“亡灵”和“碎片化”来讲述?在鲁尔福笔下,生与死的界限被彻底消解。科马拉的鬼魂并非为了制造廉价的恐怖,而是因为“他们被记忆困住了”。在这里,死亡不是终结,而是另一种被囚禁的状态。亡魂们无法安息,只能在时间的废墟中一遍遍重复生前的痛苦、罪恶与遗憾。与之相配的,是小说完全打破线性时间的碎片化叙事。过去与现在无缝交织,视角在不同人物间频繁跳跃。这种“破碎”恰恰是记忆和创伤最真实的形态。读者必须像侦探一样,在时空错乱的碎片中去还原真相。这种叙事策略极大地增强了小说的宿命感,让人深切体会到那种深陷历史泥沼、无法逃脱的窒息感。 佩德罗·巴拉莫(Pedro Páramo),这个名字本身就是一个巨大的隐喻:“石头”与“荒原”。他不仅是科马拉的绝对统治者,更是墨西哥大庄园制和考迪罗(独裁者)的化身。鲁尔福没有直接去写宏大的历史事件,而是把历史的重压写进了每一个普通人的生死和日常中。最令人震撼的,是佩德罗在失去一生挚爱苏萨娜后,选择交叉双臂,让土地荒芜,用饥饿和死亡来报复整个世界。这种“个人的绝望导致集体的毁灭”,将微观的情感创伤与宏观的时代悲剧完美融合。科马拉从繁华走向死寂,正是那个时代墨西哥农村衰败、底层人民苦难的最深刻缩影。
|
||||||
|
|
||||||
|
#### **自述与荒原:含情脉脉的绝望主宰**
|
||||||
|
|
||||||
|
这种“从单线到群像,从碎片到整体,从个人到时代”的写作手法,在文学史上有着深远的共鸣。阅读《佩德罗·巴拉莫》时,我不可遏制地想起了刘楚昕的《泥潭》。两部作品在精神内核和叙事策略上展现出了惊人的相似性。《泥潭》同样巧妙地运用了亡灵元素与破碎化的故事,通过周围人的记忆和侧面讲述,将核心人物从历史的“泥潭”中一点点打捞出来。无论是科马拉的亡魂,还是《泥潭》中那些承载着无法释怀之创伤的灵魂,它们都不仅仅是文学修辞,更是沉重历史的具象化。两位作者都不约而同地选择了打破线性时间,利用亡魂与碎片来拼凑一个完整的人,以此来刻画那个令人窒息的时代。这证明了,在面对深重的历史创伤和复杂的时代泥潭时,这种非线性的、多声部的叙事策略,具有超越文化和地域的强大普遍有效性。 “他重重地跌倒在地,像一堆石头一样垮塌了。”这是小说的结尾,佩德罗·巴拉莫迎来了他的毁灭。然而,科马拉的荒原虽已死寂,这本书却在文学史上建立了不朽的丰碑。加西亚·马尔克斯曾说他能将这本书倒背如流,因为它直接启发了《百年孤独》的诞生。 合上书本,科马拉亡魂的喃喃自语似乎仍在耳边回荡。鲁尔福用不到十万字的篇幅,为我们构建了一个深邃的文学迷宫。在这个迷宫里,我们看到的不仅是一个暴君的兴衰,更是人类在时间、记忆与历史洪流中,那份永恒的孤独与挣扎。
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
- 如果说我听到的只是一片寂静,那是因为我还不习惯于寂静,也许是我头脑中还充满着喧嚣和各种嘈杂声。是的,我的耳际确实还在鸣响着各种喧闹声。在这风平浪静的地方,这种声音听得更清楚了。这种沉重的声音此时仍停留在我的心间。我回忆起母亲对我说过的话:“到了那里,我的话你将会听得更清楚,我将离你更近。如果死亡有时也会发出声音的话,那么,你将会发现我的回忆发出的声音比我死亡发出的声音更为亲近。”我的母亲……她的声音还活着。
|
||||||
|
- “你走的那天我就明白,我再也不会见到你了。你走时晚霞将你全身映得通红,半边天都给染成血红色了。你微笑着,将这座村庄抛在身后。你曾经多次跟我谈起过这个村庄:‘我爱这个村庄,那是因为村庄里有你在;除此之外,我恨村庄里的一切,甚至我恨自己出生在这个村庄里。’我当时就想:‘她不会再回来了,她永远也不会回来了。’”
|
||||||
|
- 这个世道啊,它从四面八方把你压得紧紧的,要把我们压成齑粉,将我们弄得粉身碎骨,仿佛要用我们的鲜血浇洒大地。
|
||||||
|
- 当她感到自己的肉体破裂时,她觉得自己消失在虚空中。她那肉体像地垄一般被一枚钉子划开,这枚钉子先是炽热的,继而是温暖的,后来又是甜丝丝的。它重重地刺着她那柔软的肉体,越钉越深,越来越深,一直钉得她呻吟起来。
|
||||||
|
- 人活着就是每时每刻毁灭我们的世界。
|
||||||
|
- 感谢上帝,我们生活在一块应有尽有的土地上。可这里的一切都是酸的,我们注定如此。
|
||||||
|
- 我们的骨髓变成了火堆,我们的血管变成了火线,还要让我们以令人难以置信的痛苦来自赎,而这种痛苦永远也得不到减轻。上帝的震怒总是把这痛苦之火越拨越旺。
|
||||||
|
- 那时世间有个硕大的月亮。我看着你,看坏了眼睛。月光渗进你的脸庞,我一直看着这张脸,百看不厌,这是你的脸。它很柔和,柔过月色;你的嘴唇宛如被褶饰装饰着,十分湿润,星光把它照得色彩斑斓;你的身躯在黑夜之水中透明得发光。苏萨娜呀,苏萨娜·圣胡安。
|
||||||
|
- 在她那细雨一样的睫毛面前。灯光是散漫的,在那个人影的胸口有一束灯光,像是一颗小小的心脏,犹如闪烁的火焰在跳动。你的心难过得正在死去,她想,我知道你是来告诉我弗洛伦西奥已经死了,不过,这件事我已经知道了。你不要为他们忧虑,也不要为我断肠。我已把自己的痛苦埋藏在一个可靠的地方。可不要让你的心脏熄灭。
|
||||||
@@ -1,16 +1,61 @@
|
|||||||
# 关于页(后台「页面文案 → 关于页」可改)
|
doc:
|
||||||
# ── 一级:blocks 块式正文(按顺序渲染,可拖拽排序;某块 enabled: false 即隐藏)
|
zh: |-
|
||||||
# ── 二级:intro 多段 markdown(仅在 blocks 为空时使用)
|
<span class="font-huiwen c-blue">一个喜欢学习技术、摄影、阅读的气象僧</span>
|
||||||
# ── 三级:内置默认文案(两者都空时兜底,构建永不炸)
|
|
||||||
layout:
|
|
||||||
# 名片区(头像/名字/身份语/社交)开关:头像等数据事实源在「主页设置」
|
|
||||||
showIdCard: true
|
|
||||||
|
|
||||||
|
## 关于我
|
||||||
|
|
||||||
|
<span class="font-huiwen c-blue">
|
||||||
|
|
||||||
|
**计算机是有趣的**
|
||||||
|
|
||||||
|
**阅读也是有趣的**
|
||||||
|
|
||||||
|
**摄影亦是有趣的**
|
||||||
|
|
||||||
|
**运动更是有趣的**
|
||||||
|
|
||||||
|
**拥抱开源,热爱开源**
|
||||||
|
|
||||||
|
</span>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 找到我
|
||||||
|
|
||||||
|
- [Website](https://wait4x.online/)
|
||||||
|
- [Email](mailto:X@alumni.nudt.edu.cn)
|
||||||
|
- [GitHub](https://github.com/wait4xx)
|
||||||
|
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** 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 EXIF and a line of curation
|
||||||
|
|
||||||
|
## About me
|
||||||
|
|
||||||
|
An aspiring forecaster — meteorology was where I started, and I still read cloud charts for fun. Coding is the second thread: Linux, containers, and 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**.
|
||||||
|
|
||||||
|
## Find me
|
||||||
|
|
||||||
|
- [This site](https://wait4x.online/)
|
||||||
|
- [Email](mailto:X@alumni.nudt.edu.cn)
|
||||||
|
- [GitHub](https://github.com/wait4xx)
|
||||||
|
- [Self-hosted gitea](https://git.wait4x.online/wait4x/blog)
|
||||||
|
- [RSS](/rss.xml)
|
||||||
|
|
||||||
|
## How this site is built
|
||||||
|
|
||||||
|
Static pages by Astro, no client-side framework JS; content lives in my self-hosted gitea — the local editor and the gitea web UI write to the same repo, and a push builds the site. The typography stack (font subsetting, greyscale skeleton, dual light/dark) is all in the repo — view source freely.
|
||||||
blocks:
|
blocks:
|
||||||
- type: prose
|
- type: prose
|
||||||
width: narrow
|
enabled: true
|
||||||
body:
|
body:
|
||||||
zh: |
|
zh: |-
|
||||||
<span class="font-huiwen c-red">写作者</span> · 读者 · 快门按压者,拥抱开源。
|
<span class="font-huiwen c-red">写作者</span> · 读者 · 快门按压者,拥抱开源。
|
||||||
|
|
||||||
这里是我三条轨道的公开工作台:**读过的书**留下评注,**写过的代码**进项目墙,**按下的快门**收入图库。
|
这里是我三条轨道的公开工作台:**读过的书**留下评注,**写过的代码**进项目墙,**按下的快门**收入图库。
|
||||||
@@ -22,7 +67,7 @@ blocks:
|
|||||||
- 影像收进图库,配一点 EXIF 与一句策展
|
- 影像收进图库,配一点 EXIF 与一句策展
|
||||||
|
|
||||||
本站本身即开源,构建与发布全链都在仓里。
|
本站本身即开源,构建与发布全链都在仓里。
|
||||||
en: |
|
en: |-
|
||||||
<span class="font-huiwen c-red">Writer</span> · Reader · Shutter-presser, embracing open source.
|
<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.
|
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.
|
||||||
@@ -34,8 +79,9 @@ blocks:
|
|||||||
- Images go to the gallery, with a bit of EXIF and one line of curation
|
- 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.
|
This site is itself open source — the full build and deploy pipeline lives in the repo.
|
||||||
|
width: narrow
|
||||||
- type: tracks
|
- type: tracks
|
||||||
|
enabled: true
|
||||||
title:
|
title:
|
||||||
zh: 兴趣版图
|
zh: 兴趣版图
|
||||||
en: Interests
|
en: Interests
|
||||||
@@ -61,27 +107,87 @@ blocks:
|
|||||||
desc:
|
desc:
|
||||||
zh: 街拍与山野,随身一台无反,在等光的间隙按快门。
|
zh: 街拍与山野,随身一台无反,在等光的间隙按快门。
|
||||||
en: Street and landscapes with a mirrorless camera, pressing the shutter while waiting for the light.
|
en: Street and landscapes with a mirrorless camera, pressing the shutter while waiting for the light.
|
||||||
|
|
||||||
- type: prose
|
- type: prose
|
||||||
|
enabled: true
|
||||||
|
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**.
|
||||||
|
width: narrow
|
||||||
|
- type: linklist
|
||||||
|
enabled: true
|
||||||
|
title:
|
||||||
|
zh: 找到我
|
||||||
|
en: Find me
|
||||||
|
items:
|
||||||
|
- href: https://wait4x.online/
|
||||||
|
label:
|
||||||
|
zh: 本站
|
||||||
|
en: Website
|
||||||
|
note:
|
||||||
|
zh: 文章、图库与瞬间
|
||||||
|
en: Posts, gallery and moments
|
||||||
|
- href: mailto:X@alumni.nudt.edu.cn
|
||||||
|
label:
|
||||||
|
zh: 邮件
|
||||||
|
en: Email
|
||||||
|
note:
|
||||||
|
zh: 有话直说
|
||||||
|
en: Say hello directly
|
||||||
|
- href: https://github.com/wait4xx
|
||||||
|
label:
|
||||||
|
zh: GitHub
|
||||||
|
en: GitHub
|
||||||
|
note:
|
||||||
|
zh: 开源项目与提交
|
||||||
|
en: Open-source projects and commits
|
||||||
|
- href: https://git.wait4x.online/wait4x/blog
|
||||||
|
label:
|
||||||
|
zh: 自托管 gitea
|
||||||
|
en: Self-hosted gitea
|
||||||
|
note:
|
||||||
|
zh: 本站源码
|
||||||
|
en: Source of this site
|
||||||
|
- href: /rss.xml
|
||||||
|
label:
|
||||||
|
zh: RSS
|
||||||
|
en: RSS
|
||||||
|
note:
|
||||||
|
zh: 订阅更新
|
||||||
|
en: Subscribe to updates
|
||||||
|
- type: prose
|
||||||
|
enabled: true
|
||||||
title:
|
title:
|
||||||
zh: 本站如何构建
|
zh: 本站如何构建
|
||||||
en: How this site is built
|
en: How this site is built
|
||||||
body:
|
body:
|
||||||
zh: 页面由 Astro 静态生成,不含运行时框架 JS;内容住在自托管 gitea——本地编辑器与 gitea 网页写的是同一个 git 仓库,push 之后由 homelab 自动构建发布;评论走自托管 Artalk。页脚 built from 一行链到本次构建对应的 commit,构建过程透明可追溯。
|
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.
|
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.
|
||||||
|
width: narrow
|
||||||
# ---- 二级回退(blocks 为空时使用)----
|
layout:
|
||||||
|
showIdCard: true
|
||||||
intro:
|
intro:
|
||||||
zh: |
|
zh: |-
|
||||||
写作者 · 读者 · 快门按压者,拥抱开源。
|
写作者 · 读者 · 快门按压者,拥抱开源。
|
||||||
|
|
||||||
这里是我三条轨道的公开工作台:**读过的书**留下评注,**写过的代码**进项目墙,**按下的快门**收入图库。
|
这里是我三条轨道的公开工作台:**读过的书**留下评注,**写过的代码**进项目墙,**按下的快门**收入图库。
|
||||||
en: |
|
en: |-
|
||||||
Writer · Reader · Shutter-presser, embracing open source.
|
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.
|
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)
|
|
||||||
typography:
|
|
||||||
font: font-huiwen
|
|
||||||
accent: c-red
|
|
||||||
|
After Width: | Height: | Size: 40 KiB |
@@ -1,44 +1,21 @@
|
|||||||
# 主页身份语(后台「页面文案 → 主页设置」可改)
|
# 5n C1 后台折叠分组:平铺字段收进 identity/copy/content 三个可选组(组内压平叶子),socials 顶层原位。
|
||||||
# 全部字段留空 = 现有形态(字段级回落默认值,零配置零变化)
|
# 旧→新映射见 src/content.config.ts 的 siteHome schema 注释;本次迁移值逐字保留,零功能变化。
|
||||||
tagline:
|
identity:
|
||||||
zh: 写作者 · 读者 · 快门按压者,拥抱开源。
|
tagline_zh: 拥抱开源,热爱开源
|
||||||
en: Writer · Reader · Shutter-presser — embracing open source.
|
tagline_en: Embracing open source, loving open source.
|
||||||
|
avatar: ./assets/118a04cd0986b9c5.jpg
|
||||||
# 副标题(可选,markdown 短文本):渲染在身份语下方,留空不渲染
|
favicon: assets/118a04cd0986b9c5.jpg
|
||||||
# subtitle:
|
content:
|
||||||
# zh: '正在把**阅读、代码与影像**收进同一个工作台'
|
latestPhotos: 4
|
||||||
# en: 'Collecting **reading, code and images** into one workbench'
|
perTrackLimit: 6
|
||||||
|
sections_tracks: true
|
||||||
# 头像(可选):上传后自动写入 ./assets/avatar-xxx.jpg(走 Astro 图片管线)
|
sections_photos: true
|
||||||
# 留空 = 回退内置渐变占位
|
sections_heatmap: true
|
||||||
# avatar: ./assets/avatar.jpg
|
sections_projects: true
|
||||||
|
|
||||||
# 站点图标(favicon,全局生效):public 下的路径;留空回退 /favicon.svg
|
|
||||||
# 上传走 public/icons/(后台媒体库),可填 /icons/my.ico、/icons/my.svg
|
|
||||||
favicon: /favicon.svg
|
|
||||||
|
|
||||||
# 社交链接(顺序即展示顺序,后台可增删拖动排序)
|
|
||||||
# icon 取值:github / rss / email / bilibili / x / 留空(留空按 label 显示纯文字)
|
|
||||||
socials:
|
socials:
|
||||||
- label: GitHub
|
- label: GitHub
|
||||||
url: https://github.com/wait4xx
|
url: https://github.com/wait4xx
|
||||||
icon: github
|
icon: github
|
||||||
- label: RSS
|
|
||||||
url: /rss.xml
|
|
||||||
icon: rss
|
|
||||||
- label: 邮件
|
- label: 邮件
|
||||||
url: mailto:admin@wait4x.online
|
url: mailto:X@alumni.nudt.edu.cn
|
||||||
icon: email
|
icon: email
|
||||||
|
|
||||||
# 最新影像张数(1-6,默认 4;填 0 = 隐藏该区块)
|
|
||||||
latestPhotos: 4
|
|
||||||
|
|
||||||
# 首页每轨文章条数(4-8,默认 6)
|
|
||||||
perTrackLimit: 6
|
|
||||||
|
|
||||||
# 区块开关(默认全开;关掉的区块不渲染)
|
|
||||||
sections:
|
|
||||||
tracks: true
|
|
||||||
photos: true
|
|
||||||
heatmap: true
|
|
||||||
projects: true
|
|
||||||
@@ -1,3 +1,11 @@
|
|||||||
|
typing:
|
||||||
|
- zh: 喜欢计算机的气象僧
|
||||||
|
en: Trained in meteorology, coding for fun
|
||||||
|
- zh: 拥抱开源,热爱开源
|
||||||
|
en: Embrace open source, love open source
|
||||||
|
story:
|
||||||
|
zh: 本站本身即开源:[git.wait4x.online/wait4x/blog](https://git.wait4x.online/wait4x/blog) —— Astro SSG、双写作流、构建发布全链皆在仓中。以下是我在自托管 gitea 与 GitHub 上的全部公开仓库,以及最近一年的贡献活动。
|
||||||
|
en: 'This site is itself open source: [git.wait4x.online/wait4x/blog](https://git.wait4x.online/wait4x/blog) — 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.'
|
||||||
blurb:
|
blurb:
|
||||||
zh: 本站本身即开源:
|
zh: 本站本身即开源:
|
||||||
en: 'This site is itself open source:'
|
en: 'This site is itself open source:'
|
||||||
@@ -6,17 +14,7 @@ repo_label: git.wait4x.online/wait4x/blog
|
|||||||
blurb_after:
|
blurb_after:
|
||||||
zh: —— Astro SSG、双写作流、构建发布全链皆在仓中。以下是我在自托管 gitea 与 GitHub 上的全部公开仓库,以及最近一年的贡献活动。
|
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.
|
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=全部(默认) | only=仅 include 列出 | exclude=排除 exclude 列出
|
|
||||||
mode: all
|
mode: all
|
||||||
# 白名单(mode=only 时生效)
|
|
||||||
include: []
|
|
||||||
# 黑名单(mode=exclude 时生效)
|
|
||||||
exclude: []
|
|
||||||
# 排序:pinned-first=置顶优先(默认) | stars=星数降序 | updated=最近更新
|
|
||||||
sort: pinned-first
|
sort: pinned-first
|
||||||
# 来源过滤:both=双源合并(默认) | github | gitea
|
perSource: github
|
||||||
perSource: both
|
intro_from: 'off'
|
||||||
# 开篇文案来源:off=用本文件 blurb(默认) | github-readme=构建期取 wait4xx/wait4xx 的 README 渲染
|
|
||||||
intro_from: "off"
|
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# 页面标题下的说明文案(瞬间 / 书架):纯 markdown,后台「页面文案」直接改;
|
||||||
|
# 排版(字体/颜色)用内联类,与文章正文同一套(编辑器 Insert ▸ 排版 可视化选)。
|
||||||
|
# 删字段即回落内置默认,不破版。
|
||||||
|
|
||||||
|
moments:
|
||||||
|
note_zh: 零碎的记录:随手拍、一句话感想、开源动态。
|
||||||
|
note_en: 'Fragmented notes: casual shots, one-liners, and open-source updates.'
|
||||||
|
|
||||||
|
books:
|
||||||
|
note_zh: 点一行展开封面与简介 · 置顶的书默认展开
|
||||||
|
note_en: 'Tap a row for the cover and blurb · pinned books start open'
|
||||||
|
|
||||||
|
archives:
|
||||||
|
note_zh: 按年份折叠 · 按月分组 · 共 {n} 篇
|
||||||
|
note_en: 'Folded by year · grouped by month · {n} posts in total'
|
||||||
@@ -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
|
||||||
@@ -2,6 +2,45 @@
|
|||||||
|
|
||||||
> 适用:`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>` 里的内容可能永远没人展开
|
||||||
|
|
||||||
|
## 隐藏文章(两种写法,任选其一)
|
||||||
|
|
||||||
|
不想让某篇出现在首页/归档/书架/RSS/sitemap:
|
||||||
|
- frontmatter 写 `hidden: true`(后台有开关)
|
||||||
|
- 或在 `tags` 里加一个 `hidden`
|
||||||
|
|
||||||
|
页面本身仍会生成,链接可以直接分享给别人;想连页面都不生成,用 `draft: true`(草稿)。
|
||||||
|
**隐藏与置顶同开时,以隐藏为准**(2026-11-04 修复:此前隐藏开关是假的,隐藏+置顶会照样显示还排第一)。
|
||||||
|
|
||||||
|
## 置顶文章
|
||||||
|
|
||||||
|
首页、归档、书架三处列表默认按日期倒序。想让某篇长期排在最前,frontmatter 加一行:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
pinned: true
|
||||||
|
```
|
||||||
|
|
||||||
|
语义边界(刻意设计成这样):
|
||||||
|
- **只影响列表顺序**,不改 `pubDate`、不改 URL、不影响 RSS 的时间序列(订阅者不会看到时间错乱)
|
||||||
|
- 多篇同时置顶时,它们内部仍按日期倒序
|
||||||
|
- 列表里会显示一个灰阶「置顶」小徽标,取消置顶后立刻回到时间序
|
||||||
|
- 后台「文章」编辑页有「置顶」开关,不必手改 frontmatter
|
||||||
|
|
||||||
## 目录约定
|
## 目录约定
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -66,6 +105,12 @@ 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 预览卡)
|
||||||
|
|
||||||
**一行只放一个链接、且它独立成段**时,构建期会抓取该页的 OG 信息渲染成卡片(标题 / 描述 / 站点图标 / 可选缩略图):
|
**一行只放一个链接、且它独立成段**时,构建期会抓取该页的 OG 信息渲染成卡片(标题 / 描述 / 站点图标 / 可选缩略图):
|
||||||
@@ -153,6 +198,43 @@ https://astro.build/
|
|||||||
|
|
||||||
体积账结论(2026-11-02 实测,本地无 gzip 的相对值):**未使用某字体的页面,字体字节增量恒为 0**(构建期按页子集化 + unicode-range 按需拉取)。汇文采用文风开源的 cn-font-split 切片包(339 片),实测只拉用到字符的 19 片。
|
体积账结论(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 嵌入 | 未落地原因 |
|
| 字体 | 许可证 | 允许 webfont 嵌入 | 未落地原因 |
|
||||||
@@ -168,7 +250,7 @@ https://astro.build/
|
|||||||
| 阿里妈妈东方大楷 / 演示悠然小楷 | 免费商用 / 个人免费 | ⚠️ 需逐款核嵌入条款 | 无包;"个人免费"字体常禁 @font-face |
|
| 阿里妈妈东方大楷 / 演示悠然小楷 | 免费商用 / 个人免费 | ⚠️ 需逐款核嵌入条款 | 无包;"个人免费"字体常禁 @font-face |
|
||||||
| 霞鹜文楷 Light/Bright | OFL-1.1 | ✅ | 实测 **+8605KB**(CJK 切片粒度所致) |
|
| 霞鹜文楷 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/页 的要评估是否值得。
|
> 加新字体的流程:①核许可证与 **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
|
```markdown
|
||||||
<span class="ink-muted">次要说明</span>
|
<span class="ink-muted">次要说明</span>
|
||||||
@@ -185,6 +267,77 @@ https://astro.build/
|
|||||||
后台编辑器里:**选中文字 → Insert 菜单 → 「排版(字体/颜色)」→ 面板里点一个类**,文字即被 `<span class="…">` 包住;点已套用的片段可改类,Backspace 可整体删除。
|
后台编辑器里:**选中文字 → Insert 菜单 → 「排版(字体/颜色)」→ 面板里点一个类**,文字即被 `<span class="…">` 包住;点已套用的片段可改类,Backspace 可整体删除。
|
||||||
面板里的两类与上表一一对应(字体 8 / 颜色 9),类清单单一事实源在 `public/admin/typography-classes.js`。
|
面板里的两类与上表一一对应(字体 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 秒)
|
||||||
@@ -277,6 +430,9 @@ markdown 原生语法,无需任何配置:
|
|||||||
| `latestPhotos` | 0-6 | 4 | 「最新影像」张数;**0 = 隐藏该区块** |
|
| `latestPhotos` | 0-6 | 4 | 「最新影像」张数;**0 = 隐藏该区块** |
|
||||||
| `perTrackLimit` | 4-8 | 6 | 首页阅读/技术两轨各显示条数 |
|
| `perTrackLimit` | 4-8 | 6 | 首页阅读/技术两轨各显示条数 |
|
||||||
| `sections` | `{tracks, photos, heatmap, projects}` 布尔 | 全开 | 区块开关,关掉即不渲染 |
|
| `sections` | `{tracks, photos, heatmap, projects}` 布尔 | 全开 | 区块开关,关掉即不渲染 |
|
||||||
|
| `projectsLimit` | 1-99 | 6 | 首页项目墙条数(越界夹取) |
|
||||||
|
| `announcement` | `{zh, en}`(markdown 富文本) | 空 | 名片区下方公告;空 = 不渲染 |
|
||||||
|
| `postsTracks` | `{reading, tech, photo}` 布尔 | 全开 | 关掉某轨则该轨文章不在首页列出(归档页仍可见) |
|
||||||
|
|
||||||
越界值会被夹取(如 perTrackLimit=20 按 8 处理),不报错;**全部留空 = 现有形态不变**。
|
越界值会被夹取(如 perTrackLimit=20 按 8 处理),不报错;**全部留空 = 现有形态不变**。
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# 后台回滚指引
|
||||||
|
|
||||||
|
> **一句话版本**:改坏了内容 → 后台再保存一次对的就好;整次部署坏了 → Gitea 网页里把坏提交 Revert 掉;全站级灾难 → 找主理人执行灾备开关。
|
||||||
|
|
||||||
|
## 什么时候需要哪一层
|
||||||
|
|
||||||
|
| 情形 | 用什么 | 频率预期 |
|
||||||
|
|---|---|---|
|
||||||
|
| 某篇文章/瞬间/设置改坏了 | **后台再改一次**(Git 只是保险,不是日常工具) | 常见 |
|
||||||
|
| 一次部署把多处弄坏、说不清单点 | **Gitea 网页 Revert**(下面三步) | 偶发 |
|
||||||
|
| 静态站整体故障无法快速修复 | 灾备开关切回旧 Halo(仅主理人执行,见文末) | 理论上不会 |
|
||||||
|
|
||||||
|
## Gitea 网页 Revert 三步
|
||||||
|
|
||||||
|
1. 打开提交历史:https://git.wait4x.online/wait4x/blog/commits/branch/main
|
||||||
|
(每次后台「保存」都是一条提交,时间与描述能对上)
|
||||||
|
2. 点开**弄坏的那条提交** → 右上角 **Revert** 按钮 → 确认。Gitea 会自动生成一条反向提交并提示创建 PR/直接合并——选择直接合并到 `main`
|
||||||
|
3. 推送后 30–60 秒自动构建部署,站点回到坏提交**之前**的状态
|
||||||
|
|
||||||
|
注意:
|
||||||
|
- Revert 撤销的是**整条提交**(那一次保存改动的所有内容)。如果好改动和坏改动混在同一次保存里,别 Revert,回后台把坏的部分再改回来更精准
|
||||||
|
- 只想回退构建但保留内容?不存在这种需求场景——构建永远反映 main 最新状态
|
||||||
|
|
||||||
|
## 灾备开关(仅主理人)
|
||||||
|
|
||||||
|
`infra/deploy/rollback.sh` 是**整站切换**:把 nginx 从静态站切回旧 Halo(127.0.0.1:18090),秒级生效。这是静态站全面故障时的逃生通道,日常内容问题永远用不上它。预演:`bash infra/deploy/rollback.sh`(默认 dry-run);执行前建议先 `bash infra/deploy/rollback-dryrun.sh` 看四要素——Halo 容器若已退役,这个开关就是空枪,dryrun 一眼能看出来;执行需 root + `--exec`。
|
||||||
@@ -1,17 +1,234 @@
|
|||||||
# 管理台(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 秒自动构建上线。
|
||||||
|
|
||||||
|
## 页面说明文案(5j 新增:瞬间 / 书架)
|
||||||
|
|
||||||
|
「页面文案」新增 `content/site/pages.yaml`(后台「瞬间 / 书架 标题下的说明」):那一行字的文字(markdown,支持排版类)、
|
||||||
|
字号(11–16)、颜色(灰阶四级)、字体(12 族下拉)都在这里改,两页互相独立。
|
||||||
|
|
||||||
|
兜底行为:任何字段清空/删掉都回落内置默认;越界值(如字号填 40、颜色乱写)
|
||||||
|
会被自动夹取或回落,不会破版。
|
||||||
|
|
||||||
|
## 文章置顶(5h 新增)
|
||||||
|
|
||||||
|
文章编辑页有一个「置顶」开关。打开后这篇文章排在首页分轨、归档、书架三处列表的最前面,列表里显示一个灰阶「置顶」小徽标。
|
||||||
|
|
||||||
|
语义边界(刻意如此,别当 bug 报):
|
||||||
|
- **只影响列表顺序**:`pubDate` 不变、URL 不变、RSS 里的时间序列不变,订阅者不会看到时间错乱
|
||||||
|
- **与「隐藏」同开时以隐藏为准**:后台的「隐藏」开关(5j 起是真字段)会把这篇从列表/RSS/sitemap 里拿掉,页面本身仍可分享
|
||||||
|
- **支持多篇/多本同时置顶**:置顶组内部仍按日期倒序;书架页(/books)里每个置顶的书评都会排在前面并**默认展开**
|
||||||
|
- **置顶 + 隐藏同时打开时,以隐藏为准**(5j 修复:此前隐藏开关只写一个没人读的字段,导致隐藏+置顶的文章照样显示还排第一)
|
||||||
|
- 书架页用「↑ + 横线」的回到顶部图标表示置顶(不写文字),与右侧展开箭头形状不同,不会混读
|
||||||
|
- 关掉开关立即恢复时间序,没有"冷却期"
|
||||||
|
|
||||||
|
## 瞬间(5h 起与文章同级)
|
||||||
|
|
||||||
|
「瞬间」集合的「文字」字段是 **markdown 编辑器**,与文章同一套表达力:
|
||||||
|
|
||||||
|
- markdown:`**加粗**`、`*斜体*`、列表、`> 引用`、`` `行内代码` ``
|
||||||
|
- 排版类 HTML:`<span class="c-blue font-kai">彩色 + 楷体</span>`(可用类清单见写作指南「文章排版类」一节,与文章完全同一份白名单)
|
||||||
|
- 彩色**不再限次**(原来的"一页最多三处"已取消);仍守外壳灰阶:导航/页脚/按钮/背景不染色
|
||||||
|
|
||||||
|
## 预览窗格直接显色/显字体(5l 新增)
|
||||||
|
|
||||||
|
编辑任何 markdown 字段时,右侧预览窗格里的排版类**直接可见效果**(颜色、艺术字体都真实渲染):
|
||||||
|
|
||||||
|
- 样式来自与站点同一份类清单(后台 `typography-classes.js` 单一事实源),字体挂的是站内切片 CSS——预览里只按需拉取用到的字的切片,不会整包下载;
|
||||||
|
- 七款自托管艺术字体(汇文/小薇/黄油/马善政/柳建毛草/得意黑/新晰黑)预览为**真实字形**;思源宋/霞鹜文楷等基础字体在预览里走系统近似(楷体→Kaiti、宋体→Songti),正式页面上仍是自托管精确字形;
|
||||||
|
- 手写与面板两种写法都认:选中文字 → **Insert ▸ 排版** 打开可视化选择(字体带实样、颜色带色卡),或直接写 `<span class="font-kai c-red">楷体红</span>`(多类空格分隔)。
|
||||||
|
|
||||||
|
**给哪类内容上什么样式**:正式的排版建议(标题/引文/金句的字体颜色搭配)见 [写作指南](./写作指南.md)「文章排版类」一节——那里有全部 17 类字体、24 档颜色的完整清单与示例。
|
||||||
|
|
||||||
|
**每条瞬间自带评论区**:时间线卡片下方有「评论」折叠条,读者点开才加载评论(不点开不为它付任何加载成本)。中文页与英文页共用同一条评论流,语言切换不会丢评论。
|
||||||
|
|
||||||
|
**每条的浏览 / 评论数字是只读的**:读者点开评论区不会虚增浏览数(只有整页访问才计一次)。
|
||||||
|
|
||||||
|
## 照片说明文字(灯箱右侧)支持排版(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 字段留空即可)
|
||||||
|
|
||||||
|
## 关于页正文(5k 起:方案 A 白纸直编)
|
||||||
|
|
||||||
|
「页面文案 → 关于页」最上面是「**整页正文**」一个 markdown 字段:想写什么写什么、
|
||||||
|
想多长写多长,支持排版类(字体/颜色)与 `##` 小标题。保存后整页就渲染这一段
|
||||||
|
(身份名片仍在顶部)。
|
||||||
|
|
||||||
|
下面的「块式结构」是 legacy 通道:只有当「整页正文」清空时才会显示,
|
||||||
|
老内容已迁移进正文,平时不用管它。四级回落:整页正文 → 块式结构 → intro → 内置默认。
|
||||||
|
|
||||||
|
## 块编辑:关于页的块式页面构建器
|
||||||
|
|
||||||
|
关于页(`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)
|
||||||
|
**叙述已合一(5j 起)**:改用 `story` 字段(后台显示为「叙述(推荐改这里)」),整段写成 markdown,
|
||||||
|
链接直接内联(`[文字](url)`),不再分"链接前/链接后"。旧的两个字段(链接前/链接后)
|
||||||
|
只是 legacy——仅当「叙述」为空时才会退回旧拼接,平时不用管它们。
|
||||||
|
|
||||||
|
|
||||||
|
**开篇是原生「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 断构建期提取管线)
|
||||||
@@ -23,3 +240,18 @@
|
|||||||
|
|
||||||
- 启动时探测 /api/forgejo 报 404 属良性回落(日志噪音,无影响)
|
- 启动时探测 /api/forgejo 报 404 属良性回落(日志噪音,无影响)
|
||||||
- 大视频仍走仓外 store + /media/ 绝对路径(见写作指南体积纪律表)
|
- 大视频仍走仓外 store + /media/ 绝对路径(见写作指南体积纪律表)
|
||||||
|
|
||||||
|
|
||||||
|
## 媒体文件落点地图(5n 归置)
|
||||||
|
|
||||||
|
上传的文件各归各位,不用记——表里的「落点」由后台自动处理:
|
||||||
|
|
||||||
|
| 在哪上传 | 落到仓库哪 | 网页上从哪读 |
|
||||||
|
|---|---|---|
|
||||||
|
| 文章封面/文内图 | content/posts/assets/ | 构建期自动优化为 WebP |
|
||||||
|
| 瞬间配图 | 同上(按集合相对路径) | 同上 |
|
||||||
|
| 主页头像/关于页图 | content/site/assets/ | 构建期优化 |
|
||||||
|
| 站点图标 favicon | public/icons/ | 原样分发 |
|
||||||
|
| 图库照片 | content/photos/(摄取管线) | 灯箱走原图、列表走优化图 |
|
||||||
|
|
||||||
|
「媒体库」页面(后台左侧 Media)列出的是图库照片目录。要找某篇文章的配图,直接进那篇文章的编辑页点开图片字段即可。
|
||||||
@@ -27,16 +27,38 @@ git clean -fd --exclude node_modules
|
|||||||
SHA=$(git rev-parse --short HEAD)
|
SHA=$(git rev-parse --short HEAD)
|
||||||
echo "[build] 构建源: origin/main @ $SHA"
|
echo "[build] 构建源: origin/main @ $SHA"
|
||||||
|
|
||||||
|
# 5n B1:构建状态生命周期(后台 /admin/deploy-status.js 轮询 /admin/build-status.json)。
|
||||||
|
# building 态写在 rsync 前(此时 staging 仍服务上一版),ok 态写在末尾并解除 trap;
|
||||||
|
# 任何中途退出(set -e 炸链)由 trap 兜底写 fail——状态文件每次 rsync 后重写,不依赖 dist
|
||||||
|
mkdir -p "$STAGING/admin" 2>/dev/null || true
|
||||||
|
write_status() { printf '{"sha":"%s","time":"%s","state":"%s","pages":%s}\n' "$SHA" "$(date -Is)" "$1" "${2:-0}" > "$STAGING/admin/build-status.json" 2>/dev/null || true; }
|
||||||
|
trap 'write_status fail' EXIT
|
||||||
|
write_status building
|
||||||
|
|
||||||
pnpm install --frozen-lockfile >/dev/null
|
pnpm install --frozen-lockfile >/dev/null
|
||||||
|
|
||||||
|
# 5n A1:TMT 翻译先于构建(译文进 content/posts-en/,随同一次部署上线)。
|
||||||
|
# 守卫先行 + 守卫自身永不阻断(超时/无凭据/失败都只 WARN 并保留旧英文版)
|
||||||
|
[ -f scripts/deploy/translate-step.sh ] && bash scripts/deploy/translate-step.sh || echo "[build] translate-step 未就绪,跳过"
|
||||||
|
|
||||||
pnpm astro build
|
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 前绝不炸链
|
# 首页阻塞 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/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 脚本未就绪,跳过"
|
[ -f scripts/build-perf/inline-home-css.mjs ] && node scripts/build-perf/inline-home-css.mjs || echo "[build] inline-home 脚本未就绪,跳过"
|
||||||
|
|
||||||
|
# 5n C2:内容仪表盘(产物 dist/admin/stats.{html,json},随 rsync 上线)
|
||||||
|
[ -f scripts/build-perf/gen-admin-stats.py ] && python3 scripts/build-perf/gen-admin-stats.py dist || echo "[build] stats 生成器未就绪,跳过"
|
||||||
|
|
||||||
|
# README 动效资产(5i 引入、5j T2 退役):贡献蛇与 streak 快照已删——连续贡献改为
|
||||||
|
# 热力图同源本地计算(见 src/lib/heatmap.ts 的 streakStats),界面上标注"数据更新于"。
|
||||||
|
# fetch-readme-assets.mjs 保留骨架但 ASSETS 已清空,如将来要加新的自托管资产再启用。
|
||||||
|
|
||||||
# 内容 lint(T3):断链/缺图在部署前拦截;CONTENT_LINT=skip|warn 逃生门见脚本头注
|
# 内容 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 未就绪,跳过"
|
[ -f scripts/build-perf/content-lint.mjs ] && node scripts/build-perf/content-lint.mjs dist || echo "[build] content-lint 未就绪,跳过"
|
||||||
|
|
||||||
@@ -46,7 +68,10 @@ rsync -a --delete "$BUILD_REPO/dist/" "$STAGING/"
|
|||||||
ORIG="$HOME/docker_apps/blog/photos-originals"
|
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"
|
PAGES=$(find "$STAGING" -name '*.html' | wc -l)
|
||||||
|
echo "[build] done: $PAGES html files @ $SHA"
|
||||||
|
write_status ok "$PAGES"
|
||||||
|
trap - EXIT
|
||||||
|
|
||||||
# 生产冒烟(T3):staging 三断言。首版容忍失败(robots 待 T1 落地,smoke FAIL 属预期),
|
# 生产冒烟(T3):staging 三断言。首版容忍失败(robots 待 T1 落地,smoke FAIL 属预期),
|
||||||
# T1 收编后改硬门:去掉 `|| echo` 让断言失败直接炸构建
|
# T1 收编后改硬门:去掉 `|| echo` 让断言失败直接炸构建
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ collections:
|
|||||||
label_singular: 文章
|
label_singular: 文章
|
||||||
folder: content/posts
|
folder: content/posts
|
||||||
create: true
|
create: true
|
||||||
|
hint: 新文章可从置顶的《写作模板》复制起手(列表打开模板 → ⋯ 菜单 → Duplicate);英文版构建时自动机翻上线,无需在此维护
|
||||||
# 中文主稿;英文为 TMT 预翻译入仓的 .en.md(posts-en 集合不在后台,防误改译文)
|
# 中文主稿;英文为 TMT 预翻译入仓的 .en.md(posts-en 集合不在后台,防误改译文)
|
||||||
slug: '{{slug}}'
|
slug: '{{slug}}'
|
||||||
summary: '{{pubDate}} · {{title}}'
|
summary: '{{pubDate}} · {{title}}'
|
||||||
@@ -34,13 +35,14 @@ collections:
|
|||||||
media_folder: assets
|
media_folder: assets
|
||||||
public_folder: ./assets
|
public_folder: ./assets
|
||||||
fields:
|
fields:
|
||||||
- { label: 标题, name: title, widget: string }
|
- { label: 标题, name: title, widget: string, hint: '建议 ≤32 字(搜索结果超长会截断)' }
|
||||||
- { label: 日期, name: pubDate, widget: datetime, date_format: 'YYYY-MM-DD', time_format: false, format: 'YYYY-MM-DD' }
|
- { 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: category, widget: select, options: [{label: 阅读, value: reading}, {label: 技术, value: tech}, {label: 照片, value: photo}] }
|
||||||
- { label: 标签, name: tags, widget: list, required: false }
|
- { label: 标签, name: tags, widget: list, required: true, hint: '至少一个;优先复用现有标签:read(书评)/ 写作 / 站点更新——避免同义新造(如 阅读/read 分家);schema 必填,漏了构建会拦' }
|
||||||
|
- { label: 置顶, name: pinned, widget: boolean, required: false, default: false, hint: '置顶文章排在首页/归档/书架最前;只影响顺序,不改日期、URL 与 RSS 时间语义' }
|
||||||
- { label: 情绪色(书评色脊), name: mood, widget: color, required: false, hint: '#RRGGBB,仅阅读轨使用' }
|
- { label: 情绪色(书评色脊), name: mood, widget: color, required: false, hint: '#RRGGBB,仅阅读轨使用' }
|
||||||
- { label: 原书作者, name: bookAuthor, widget: string, required: false }
|
- { label: 原书作者, name: bookAuthor, widget: string, required: false }
|
||||||
- { label: 一句话描述(书架策展语), name: description, widget: text, required: false }
|
- { label: 一句话描述(书架策展语), name: description, widget: text, required: false, hint: '60–120 字最佳(搜索摘要与列表导语共用)' }
|
||||||
- { label: 书封图, name: cover, widget: image, required: false, hint: '上传后自动生成,无需手填路径;仅阅读轨展示' }
|
- { label: 书封图, name: cover, widget: image, required: false, hint: '上传后自动生成,无需手填路径;仅阅读轨展示' }
|
||||||
- label: 附视频(可选)
|
- label: 附视频(可选)
|
||||||
name: video
|
name: video
|
||||||
@@ -54,12 +56,45 @@ collections:
|
|||||||
widget: string
|
widget: string
|
||||||
required: false
|
required: false
|
||||||
hint: '填 /media/xxx.jpg(先上传到 public/media 取路径);缺省浏览器自取首帧'
|
hint: '填 /media/xxx.jpg(先上传到 public/media 取路径);缺省浏览器自取首帧'
|
||||||
- { label: 隐藏(不进列表/sitemap), name: hiddenTag, widget: boolean, required: false, hint: '对应 tags 含 hidden;保存后请人工核对' }
|
- label: 隐藏(不进列表 / RSS / sitemap)
|
||||||
|
name: hidden
|
||||||
|
widget: boolean
|
||||||
|
required: false
|
||||||
|
default: false
|
||||||
|
hint: '打开后这篇不进首页/归档/书架/RSS/sitemap,但页面本身仍生成、链接可分享(想彻底不生成请用草稿)。与「置顶」同时打开时以隐藏为准——2026-11-04 修:旧开关只写一个没人读的字段,导致隐藏+置顶的文章照样显示还排第一'
|
||||||
- { label: 草稿, name: draft, widget: boolean, required: false, default: false }
|
- { label: 草稿, name: draft, widget: boolean, required: false, default: false }
|
||||||
- { label: 正文, name: body, widget: markdown }
|
- { label: 正文, name: body, widget: markdown, hint: '排版:选中文字 → Insert ▸ 排版(字体/颜色可视化选);或或手写 <span class="font-kai c-red c-red">…</span>。字体 17 类、颜色 24 档,清单见写作指南「文章排版类」' }
|
||||||
|
|
||||||
# 瞬间:随手发(text/image 至少其一——Sveltia 无法表达条件必填,两者都设可选,
|
# 主题旋钮(样式即数据 · 层 4):只放白名单内的 9 组,越界值会被构建期钳制
|
||||||
# 保存前自查;pubDate 必填)。图片走图库媒体库(content/photos 同源可复用)
|
- 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
|
- name: moments
|
||||||
label: 瞬间
|
label: 瞬间
|
||||||
folder: content/moments
|
folder: content/moments
|
||||||
@@ -69,12 +104,18 @@ collections:
|
|||||||
sortable_fields: [pubDate, text]
|
sortable_fields: [pubDate, text]
|
||||||
fields:
|
fields:
|
||||||
- { label: 时间, name: pubDate, widget: datetime, date_format: 'YYYY-MM-DD', time_format: false, format: 'YYYY-MM-DD' }
|
- { label: 时间, name: pubDate, widget: datetime, date_format: 'YYYY-MM-DD', time_format: false, format: 'YYYY-MM-DD' }
|
||||||
- { label: 文字, name: text, widget: text, required: false, hint: '一句话(与图片至少填一项)' }
|
- label: 文字
|
||||||
- { label: 图片, name: image, widget: image, required: false, hint: '与文字至少填一项;建议先压缩' }
|
name: text
|
||||||
|
widget: markdown
|
||||||
|
required: false
|
||||||
|
hint: '与图片至少填一项。支持 markdown 与排版类 HTML,例如 <span class="c-blue font-kai">彩色+楷体</span>;选中文字 → Insert ▸ 排版 可视化选字体/颜色(预览窗格直接显色显字体)。装饰色仍守“外壳灰阶、彩色只作强调”的纪律'
|
||||||
|
- { label: 图片, name: image, widget: image, required: false, hint: '与文字至少填一项;上传后构建自动优化(WebP),原图无需手工压缩' }
|
||||||
- label: 标签
|
- label: 标签
|
||||||
name: tags
|
name: tags
|
||||||
widget: list
|
widget: list
|
||||||
required: false
|
required: true
|
||||||
|
- { label: 置顶, name: pinned, widget: boolean, required: false, default: false, hint: '置顶瞬间排时间线最前(可多置顶,之间按日期倒序);只影响顺序不改日期' }
|
||||||
|
- { label: 隐藏, name: hidden, widget: boolean, required: false, default: false, hint: '隐藏后从时间线移除;评论数据保留在服务端,关掉开关即恢复' }
|
||||||
- name: photos
|
- name: photos
|
||||||
label: 图库照片
|
label: 图库照片
|
||||||
folder: content/photos
|
folder: content/photos
|
||||||
@@ -91,7 +132,7 @@ collections:
|
|||||||
# 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: string, hint: '小写英文短词(如 landscape / street / portrait);新分组直接输入新词,图库筛选会自动出现' }
|
||||||
- { label: 替代文本, name: alt, widget: string, required: false }
|
- { label: 替代文本, name: alt, widget: string, required: false }
|
||||||
- { label: 人为描述, name: caption, widget: text, required: false, hint: 展示在灯箱里的人为描述(一句话策展文案) }
|
- { label: 人为描述, name: caption, widget: text, required: false, hint: 展示在灯箱里的人为描述(一句话策展文案) }
|
||||||
- label: EXIF 覆盖(一般留空,构建期自动从图提取)
|
- label: EXIF 覆盖(一般留空,构建期自动从图提取)
|
||||||
@@ -110,79 +151,117 @@ collections:
|
|||||||
- name: site_pages
|
- name: site_pages
|
||||||
label: 页面文案
|
label: 页面文案
|
||||||
files:
|
files:
|
||||||
|
- name: pages
|
||||||
|
label: 瞬间 / 书架 标题下的说明
|
||||||
|
file: content/site/pages.yaml
|
||||||
|
format: yaml
|
||||||
|
hint: 这两页大标题下面那一行字的文字;字体/颜色用编辑器 Insert ▸ 排版 内联标注(预览直接可见)
|
||||||
|
fields:
|
||||||
|
- label: 瞬间页说明
|
||||||
|
name: moments
|
||||||
|
widget: object
|
||||||
|
required: false
|
||||||
|
fields:
|
||||||
|
- { label: 中文, name: note_zh, widget: markdown, required: false, hint: '一行小字;排版(字体/颜色)选中文字 → Insert ▸ 排版' }
|
||||||
|
- { label: English, name: note_en, widget: markdown, required: false }
|
||||||
|
- label: 书架页说明
|
||||||
|
name: books
|
||||||
|
widget: object
|
||||||
|
required: false
|
||||||
|
fields:
|
||||||
|
- { label: 中文, name: note_zh, widget: markdown, required: false, hint: '一行小字;排版(字体/颜色)选中文字 → Insert ▸ 排版' }
|
||||||
|
- { label: English, name: note_en, widget: markdown, required: false }
|
||||||
|
- label: 文章页说明
|
||||||
|
name: archives
|
||||||
|
widget: object
|
||||||
|
required: false
|
||||||
|
fields:
|
||||||
|
- { label: 中文, name: note_zh, widget: markdown, required: false, hint: '一行小字;{n} 会替换为当前可见文章数(构建期);排版用 Insert ▸ 排版' }
|
||||||
|
- { label: English, name: note_en, widget: markdown, required: false }
|
||||||
- name: home
|
- name: home
|
||||||
label: 主页设置
|
label: 主页设置
|
||||||
file: content/site/home.yaml
|
file: content/site/home.yaml
|
||||||
|
format: yaml
|
||||||
# 头像上传落 content/site/assets/,yaml 写 ./assets/xxx(Astro 图片管线优化)
|
# 头像上传落 content/site/assets/,yaml 写 ./assets/xxx(Astro 图片管线优化)
|
||||||
media_folder: content/site/assets
|
media_folder: assets
|
||||||
public_folder: ./assets
|
public_folder: ./assets
|
||||||
fields:
|
fields:
|
||||||
- label: 身份语
|
- label: 身份名片
|
||||||
name: tagline
|
name: identity
|
||||||
- label: 页面排版覆盖(可选)
|
|
||||||
name: typography
|
|
||||||
widget: object
|
widget: object
|
||||||
required: false
|
required: false
|
||||||
hint: 本页正文默认字体 / 强调色 / 字号缩放(0.8–1.25);不填用全站默认,只影响本页
|
collapsed: true
|
||||||
|
hint: 头像、身份语、副标题与站点图标(首页名片区 + 浏览器标签图标)
|
||||||
fields:
|
fields:
|
||||||
|
- { label: 身份语(中文), name: tagline_zh, widget: string, required: false, hint: '首页大字身份语(markdown 富文本);可写 <span class="font-kai c-blue">…</span> 做局部强调' }
|
||||||
|
- { label: 身份语(英文), name: tagline_en, widget: string, required: false }
|
||||||
|
- { label: 副标题(中文,markdown 短文本), name: subtitle_zh, widget: string, required: false, hint: '渲染在身份语下方;留空不渲染' }
|
||||||
|
- { label: 副标题(英文), name: subtitle_en, widget: string, required: false }
|
||||||
|
- label: 头像(可选)
|
||||||
|
name: avatar
|
||||||
|
widget: image
|
||||||
|
required: false
|
||||||
|
hint: '留空 = 内置渐变占位;上传后自动优化为 60px WebP'
|
||||||
|
- label: 站点图标 favicon
|
||||||
|
name: favicon
|
||||||
|
widget: image
|
||||||
|
required: false
|
||||||
|
# media_folder 按 file 集合语义相对 content/site/ 解析(写 content/site/assets 会嵌套翻倍——5l 实测教训)
|
||||||
|
media_folder: ../../public/icons
|
||||||
|
public_folder: /icons
|
||||||
|
hint: '直接上传 .svg/.ico/.png(建议 64–512px 方图);留空回退 /favicon.svg'
|
||||||
|
- label: 文案与排版
|
||||||
|
name: copy
|
||||||
|
widget: object
|
||||||
|
required: false
|
||||||
|
collapsed: true
|
||||||
|
hint: 公告(渲染在名片区下方,留空不显示)+ 页面排版覆盖(本页正文默认字体/强调色/字号缩放,不填用全站默认,只影响主页)
|
||||||
|
fields:
|
||||||
|
- { label: 公告(中文,markdown), name: announcement_zh, widget: markdown, required: false, hint: '渲染在名片区下方;留空则不显示公告块' }
|
||||||
|
- { label: 公告(英文), name: announcement_en, widget: markdown, required: false }
|
||||||
- label: 正文字体类
|
- label: 正文字体类
|
||||||
name: font
|
name: typography_font
|
||||||
widget: select
|
widget: select
|
||||||
required: false
|
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]
|
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: 强调色类
|
- label: 强调色类
|
||||||
name: accent
|
name: typography_accent
|
||||||
widget: select
|
widget: select
|
||||||
required: false
|
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]
|
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 }
|
- { label: 字号缩放, name: typography_scale, widget: number, required: false, value_type: float, min: 0.8, max: 1.25, step: 0.05, hint: '0.8–1.25;1 = 全站默认' }
|
||||||
widget: object
|
- label: 内容与开关
|
||||||
fields:
|
name: content
|
||||||
- { label: 中文, name: zh, widget: string, required: false }
|
|
||||||
- { label: 英文, name: en, widget: string, required: false }
|
|
||||||
- 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
|
widget: object
|
||||||
required: false
|
required: false
|
||||||
hint: 关掉的区块不渲染(默认全开)
|
collapsed: true
|
||||||
|
hint: 各区块条数与显隐开关;关掉的区块不渲染,越界数值自动夹取(默认 = 现状)
|
||||||
fields:
|
fields:
|
||||||
- { label: 文章列表, name: tracks, widget: boolean, required: false, default: true }
|
- label: 最新影像张数
|
||||||
- { label: 最新影像, name: photos, widget: boolean, required: false, default: true }
|
name: latestPhotos
|
||||||
- { label: 贡献热力图, name: heatmap, widget: boolean, required: false, default: true }
|
widget: number
|
||||||
- { label: 项目墙, name: projects, widget: boolean, required: false, default: true }
|
required: false
|
||||||
- label: 头像(可选)
|
default: 4
|
||||||
name: avatar
|
min: 0
|
||||||
widget: image
|
max: 6
|
||||||
required: false
|
hint: '1-6;填 0 = 隐藏「最新影像」区块。留空默认 4'
|
||||||
hint: '留空 = 内置渐变占位;上传后自动优化为 60px WebP'
|
- label: 首页每轨文章条数
|
||||||
- label: 站点图标 favicon
|
name: perTrackLimit
|
||||||
name: favicon
|
widget: number
|
||||||
widget: string
|
required: false
|
||||||
required: false
|
default: 6
|
||||||
hint: 'public 下的路径,如 /icons/my.ico 或 /icons/my.svg;留空回退 /favicon.svg。经「图标」媒体库上传'
|
min: 4
|
||||||
|
max: 8
|
||||||
|
hint: 阅读轨/技术轨各显示条数,留空默认 6
|
||||||
|
- { label: 项目墙条数, name: projectsLimit, widget: number, required: false, value_type: int, min: 1, max: 99, hint: '首页项目墙显示几条(1–99,缺省 6)' }
|
||||||
|
- { label: 阅读轨, name: postsTracks_reading, widget: boolean, required: false, default: true, hint: '关掉则阅读轨文章不在首页列出(归档页仍可见);三轨全开 = 现状' }
|
||||||
|
- { label: 技术轨, name: postsTracks_tech, widget: boolean, required: false, default: true }
|
||||||
|
- { label: 照片轨, name: postsTracks_photo, widget: boolean, required: false, default: true }
|
||||||
|
- { label: 显示文章列表, name: sections_tracks, widget: boolean, required: false, default: true }
|
||||||
|
- { label: 显示最新影像, name: sections_photos, widget: boolean, required: false, default: true }
|
||||||
|
- { label: 显示贡献热力图, name: sections_heatmap, widget: boolean, required: false, default: true }
|
||||||
|
- { label: 显示项目墙, name: sections_projects, widget: boolean, required: false, default: true }
|
||||||
- label: 社交链接(顺序即展示顺序)
|
- label: 社交链接(顺序即展示顺序)
|
||||||
name: socials
|
name: socials
|
||||||
widget: list
|
widget: list
|
||||||
@@ -197,14 +276,24 @@ collections:
|
|||||||
required: false
|
required: false
|
||||||
options: [github, rss, email, bilibili, x]
|
options: [github, rss, email, bilibili, x]
|
||||||
hint: '不在列表里的留空 → 显示纯文字按钮'
|
hint: '不在列表里的留空 → 显示纯文字按钮'
|
||||||
|
|
||||||
- name: about
|
- name: about
|
||||||
label: 关于页
|
label: 关于页
|
||||||
file: content/site/about.yaml
|
file: content/site/about.yaml
|
||||||
|
format: yaml
|
||||||
# 块内图片上传落 content/site/assets/,yaml 写 ./assets/xxx(Astro 图片管线)
|
# 块内图片上传落 content/site/assets/,yaml 写 ./assets/xxx(Astro 图片管线)
|
||||||
media_folder: content/site/assets
|
media_folder: assets
|
||||||
public_folder: ./assets
|
public_folder: ./assets
|
||||||
fields:
|
fields:
|
||||||
- label: 页面块(拖拽排序;每块可折叠)
|
- label: 整页正文(推荐改这里,方案 A 白纸直编)
|
||||||
|
name: doc
|
||||||
|
widget: object
|
||||||
|
required: false
|
||||||
|
hint: '整页就这一个 markdown 字段:想写什么写什么、想多长写多长(支持排版类与 ## 标题)。选中文字 → Insert ▸ 排版 可视化选字体/颜色,预览窗格直接显色显字体。清空则回落到下面的 blocks 结构化通道'
|
||||||
|
fields:
|
||||||
|
- { label: 中文, name: zh, widget: markdown, required: false }
|
||||||
|
- { label: English, name: en, widget: markdown, required: false }
|
||||||
|
- label: 块式结构(legacy,整页正文为空时才显示)
|
||||||
name: blocks
|
name: blocks
|
||||||
widget: list
|
widget: list
|
||||||
typeKey: type
|
typeKey: type
|
||||||
@@ -215,6 +304,7 @@ collections:
|
|||||||
- label: 正文(markdown)
|
- label: 正文(markdown)
|
||||||
name: prose
|
name: prose
|
||||||
widget: object
|
widget: object
|
||||||
|
hint: markdown 正文块;可选小标题与版心/通栏宽度,长文分段改内容用
|
||||||
fields:
|
fields:
|
||||||
- { label: 显示, name: enabled, widget: boolean, required: false, default: true }
|
- { label: 显示, name: enabled, widget: boolean, required: false, default: true }
|
||||||
- label: 小标题(可选)
|
- label: 小标题(可选)
|
||||||
@@ -239,6 +329,7 @@ collections:
|
|||||||
- label: 版图(图标+名称+描述)
|
- label: 版图(图标+名称+描述)
|
||||||
name: tracks
|
name: tracks
|
||||||
widget: object
|
widget: object
|
||||||
|
hint: 版图块:图标 + 名称 + 描述,适合「几条主线/能力面」式介绍
|
||||||
fields:
|
fields:
|
||||||
- { label: 显示, name: enabled, widget: boolean, required: false, default: true }
|
- { label: 显示, name: enabled, widget: boolean, required: false, default: true }
|
||||||
- label: 标题
|
- label: 标题
|
||||||
@@ -274,6 +365,7 @@ collections:
|
|||||||
- label: 卡片(2/3 列)
|
- label: 卡片(2/3 列)
|
||||||
name: cards
|
name: cards
|
||||||
widget: object
|
widget: object
|
||||||
|
hint: 卡片墙(2/3 列):每卡 = 标题 + 描述 + 可选链接/角标
|
||||||
fields:
|
fields:
|
||||||
- { label: 显示, name: enabled, widget: boolean, required: false, default: true }
|
- { label: 显示, name: enabled, widget: boolean, required: false, default: true }
|
||||||
- label: 标题(可选)
|
- label: 标题(可选)
|
||||||
@@ -313,6 +405,7 @@ collections:
|
|||||||
- label: 时间线
|
- label: 时间线
|
||||||
name: timeline
|
name: timeline
|
||||||
widget: object
|
widget: object
|
||||||
|
hint: 时间线:一条时间 + 标题 + 描述;时间可写自由文本(如 2024 — 2026)
|
||||||
fields:
|
fields:
|
||||||
- { label: 显示, name: enabled, widget: boolean, required: false, default: true }
|
- { label: 显示, name: enabled, widget: boolean, required: false, default: true }
|
||||||
- label: 标题(可选)
|
- label: 标题(可选)
|
||||||
@@ -344,6 +437,7 @@ collections:
|
|||||||
- label: 引文
|
- label: 引文
|
||||||
name: quote
|
name: quote
|
||||||
widget: object
|
widget: object
|
||||||
|
hint: 单句引文 + 可选署名;配 font-kai 排版效果更好
|
||||||
fields:
|
fields:
|
||||||
- { label: 显示, name: enabled, widget: boolean, required: false, default: true }
|
- { label: 显示, name: enabled, widget: boolean, required: false, default: true }
|
||||||
- label: 引文
|
- label: 引文
|
||||||
@@ -362,6 +456,7 @@ collections:
|
|||||||
- label: 图片
|
- label: 图片
|
||||||
name: figure
|
name: figure
|
||||||
widget: object
|
widget: object
|
||||||
|
hint: 单图 + 可选说明;图片只能后台上传(落 content/site/assets),禁外链
|
||||||
fields:
|
fields:
|
||||||
- { label: 显示, name: enabled, widget: boolean, required: false, default: true }
|
- { label: 显示, name: enabled, widget: boolean, required: false, default: true }
|
||||||
- { label: 图片(必填,请用后台上传), name: image, widget: image, hint: '禁止填外链:只能上传(落 content/site/assets/),走 Astro 图片管线' }
|
- { label: 图片(必填,请用后台上传), name: image, widget: image, hint: '禁止填外链:只能上传(落 content/site/assets/),走 Astro 图片管线' }
|
||||||
@@ -376,6 +471,7 @@ collections:
|
|||||||
- label: 链接清单
|
- label: 链接清单
|
||||||
name: linklist
|
name: linklist
|
||||||
widget: object
|
widget: object
|
||||||
|
hint: 链接清单(标签 + 链接 + 备注),适合「相关阅读 / 外部资源」
|
||||||
fields:
|
fields:
|
||||||
- { label: 显示, name: enabled, widget: boolean, required: false, default: true }
|
- { label: 显示, name: enabled, widget: boolean, required: false, default: true }
|
||||||
- label: 标题(可选)
|
- label: 标题(可选)
|
||||||
@@ -407,6 +503,7 @@ collections:
|
|||||||
- label: 行动按钮
|
- label: 行动按钮
|
||||||
name: cta
|
name: cta
|
||||||
widget: object
|
widget: object
|
||||||
|
hint: 行动按钮(文案 + 链接 + 按钮字);一页建议不超过一个
|
||||||
fields:
|
fields:
|
||||||
- { label: 显示, name: enabled, widget: boolean, required: false, default: true }
|
- { label: 显示, name: enabled, widget: boolean, required: false, default: true }
|
||||||
- label: 说明文字
|
- label: 说明文字
|
||||||
@@ -425,6 +522,7 @@ collections:
|
|||||||
- label: 分隔
|
- label: 分隔
|
||||||
name: divider
|
name: divider
|
||||||
widget: object
|
widget: object
|
||||||
|
hint: 分隔块:细分隔线或纯留白,给长页分段
|
||||||
fields:
|
fields:
|
||||||
- { label: 显示, name: enabled, widget: boolean, required: false, default: true }
|
- { label: 显示, name: enabled, widget: boolean, required: false, default: true }
|
||||||
- label: 样式
|
- label: 样式
|
||||||
@@ -458,7 +556,7 @@ collections:
|
|||||||
name: font
|
name: font
|
||||||
widget: select
|
widget: select
|
||||||
required: false
|
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]
|
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: 强调色类
|
- label: 强调色类
|
||||||
name: accent
|
name: accent
|
||||||
widget: select
|
widget: select
|
||||||
@@ -468,9 +566,33 @@ collections:
|
|||||||
- name: opensource
|
- name: opensource
|
||||||
label: 开源页
|
label: 开源页
|
||||||
file: content/site/opensource.yaml
|
file: content/site/opensource.yaml
|
||||||
|
format: yaml
|
||||||
fields:
|
fields:
|
||||||
- label: 叙述(链接前)
|
- 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: story
|
||||||
|
widget: object
|
||||||
|
required: false
|
||||||
|
hint: '整段叙述写成一个字段即可(markdown + 排版类),链接直接写在句子里:[本站源码](https://git.wait4x.online/wait4x/blog) —— 这样就不用再分"链接前/链接后"。留空时才会退回下面的旧字段拼接'
|
||||||
|
fields:
|
||||||
|
- { label: 中文, name: zh, widget: markdown, required: false }
|
||||||
|
- { label: English, name: en, widget: markdown, required: false }
|
||||||
|
- label: 叙述(旧:链接前)
|
||||||
name: blurb
|
name: blurb
|
||||||
|
widget: object
|
||||||
|
required: false
|
||||||
|
hint: 'legacy:仅当上面的「叙述」为空时才使用(历史字段,为把仓库链接夹在中间而拆成两半)'
|
||||||
|
fields:
|
||||||
|
- { label: 中文, name: zh, widget: string, required: false }
|
||||||
|
- { label: 英文, name: en, widget: string, required: false }
|
||||||
- label: 页面排版覆盖(可选)
|
- label: 页面排版覆盖(可选)
|
||||||
name: typography
|
name: typography
|
||||||
widget: object
|
widget: object
|
||||||
@@ -481,17 +603,14 @@ collections:
|
|||||||
name: font
|
name: font
|
||||||
widget: select
|
widget: select
|
||||||
required: false
|
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]
|
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: 强调色类
|
- label: 强调色类
|
||||||
name: accent
|
name: accent
|
||||||
widget: select
|
widget: select
|
||||||
required: false
|
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]
|
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 }
|
- { 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 = 全站默认' }
|
||||||
widget: object
|
|
||||||
fields:
|
|
||||||
- { label: 中文, name: zh, widget: string, required: false }
|
|
||||||
- { label: 英文, name: en, widget: string, required: false }
|
|
||||||
- { label: 仓库链接地址, name: repo_url, widget: string, required: false }
|
- { label: 仓库链接地址, name: repo_url, widget: string, required: false }
|
||||||
- { label: 仓库链接文字, name: repo_label, widget: string, required: false }
|
- { label: 仓库链接文字, name: repo_label, widget: string, required: false }
|
||||||
- label: 叙述(链接后)
|
- label: 叙述(链接后)
|
||||||
|
|||||||
@@ -0,0 +1,161 @@
|
|||||||
|
/**
|
||||||
|
* 后台部署状态条(5n B1):Sveltia 管理页右下角浮层,轮询站点构建状态。
|
||||||
|
* spec:.spec/specs/2026-11-05_blog-5n-admin-deep-optimization(T4)
|
||||||
|
*
|
||||||
|
* JSON 接口契约(build.sh 每次部署写 /admin/build-status.json):
|
||||||
|
* { "sha": "a555a3e…", // 部署 commit(展示取前 7 位)
|
||||||
|
* "time": "2026-11-05T19:20:25+08:00", // 构建完成时刻(展示取 HH:MM)
|
||||||
|
* "state": "ok" | "building" | "fail",
|
||||||
|
* "pages": 53 } // 页面数(仪表盘消费,本条不读)
|
||||||
|
* 文件缺失 / 响应非 200 / JSON 解析失败 / state 非法 → 视为「不可知」:
|
||||||
|
* 胶囊淡出、不报错,下一轮照常再试——状态条是增益件,接口坏了不能吵主人。
|
||||||
|
*
|
||||||
|
* 为什么轮询而不是 SSE/WebSocket:本站是纯静态托管(CDN 上没有可驻留的
|
||||||
|
* 服务端进程,无推送通道);「保存→看部署结果」的节奏里 10s 间隔已足够
|
||||||
|
* 感知,且零服务端成本、零额外基础设施。
|
||||||
|
*/
|
||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
if (window.DEPLOY_STATUS) return; // 防重复初始化:脚本被引入两次时静默退出
|
||||||
|
var POLL_MS = 10000;
|
||||||
|
var timer = null; // null = 停表中(初始 / 页面隐藏)
|
||||||
|
|
||||||
|
var STATES = {
|
||||||
|
ok: { bg: 'rgba(28,30,34,.92)', label: '已上线' },
|
||||||
|
building: { bg: 'rgba(52,84,209,.92)', label: '构建中…' },
|
||||||
|
fail: { bg: 'rgba(179,32,32,.92)', label: '构建失败' },
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 构建中呼吸动画的 keyframes 无法用元素内联样式表达,注入一次 <style>(仍零外部资源) */
|
||||||
|
function injectKeyframes() {
|
||||||
|
var style = document.createElement('style');
|
||||||
|
style.id = 'deploy-status-style';
|
||||||
|
style.textContent = '@keyframes deploy-status-breath{0%,100%{opacity:.7}50%{opacity:1}}';
|
||||||
|
document.head.appendChild(style);
|
||||||
|
return style;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createPill() {
|
||||||
|
var el = document.createElement('div');
|
||||||
|
el.setAttribute('role', 'status'); // 状态变化对读屏器温和播报
|
||||||
|
// 全部样式内联在 JS 里:不依赖外部 CSS,单文件即可被任意页面引入
|
||||||
|
el.style.cssText = [
|
||||||
|
'position:fixed',
|
||||||
|
'right:14px',
|
||||||
|
'bottom:14px',
|
||||||
|
'z-index:10000',
|
||||||
|
'padding:6px 12px',
|
||||||
|
'font:12px/1.5 system-ui,-apple-system,"Segoe UI",sans-serif',
|
||||||
|
'color:#fff',
|
||||||
|
'border-radius:999px',
|
||||||
|
'box-shadow:0 2px 10px rgba(0,0,0,.18)',
|
||||||
|
'user-select:none',
|
||||||
|
'opacity:0', // 初始隐身,拿到合法数据才淡入
|
||||||
|
'transition:opacity .2s,background-color .2s', // 出现与状态切换均为 200ms 过渡
|
||||||
|
'pointer-events:none', // 默认不挡下层 UI;仅 fail 态(可点击)放开
|
||||||
|
].join(';');
|
||||||
|
el.addEventListener('click', function () {
|
||||||
|
// 只有 fail 态放开 pointer-events,此监听只会在「构建失败」时被触发
|
||||||
|
location.reload();
|
||||||
|
});
|
||||||
|
document.body.appendChild(el);
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 从 ISO 串直接截 HH:MM 而不走 Date 本地化:展示构建机墙钟时间,不随浏览器时区漂移 */
|
||||||
|
function hhmm(iso) {
|
||||||
|
var m = /T(\d{2}:\d{2})/.exec(String(iso || ''));
|
||||||
|
if (m) return m[1];
|
||||||
|
var d = new Date(iso); // 兜底:万一 build.sh 改用非 ISO 格式,退本地时区展示
|
||||||
|
if (isNaN(d.getTime())) return '';
|
||||||
|
var pad = function (n) { return (n < 10 ? '0' : '') + n; };
|
||||||
|
return pad(d.getHours()) + ':' + pad(d.getMinutes());
|
||||||
|
}
|
||||||
|
|
||||||
|
function boot() {
|
||||||
|
var el = createPill();
|
||||||
|
|
||||||
|
function hide() {
|
||||||
|
el.style.opacity = '0';
|
||||||
|
el.style.pointerEvents = 'none';
|
||||||
|
el.style.animation = 'none';
|
||||||
|
el.title = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function render(data) {
|
||||||
|
var conf = STATES[data && data.state];
|
||||||
|
if (!conf) { // 不可知 = 淡出静默,不区分缺文件/坏 JSON/非法枚举(对用户都是同一件事)
|
||||||
|
hide();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (data.state === 'ok') {
|
||||||
|
var parts = [];
|
||||||
|
var sha7 = String(data.sha || '').slice(0, 7);
|
||||||
|
if (sha7) parts.push(sha7);
|
||||||
|
var t = hhmm(data.time);
|
||||||
|
if (t) parts.push(t);
|
||||||
|
el.textContent = parts.length ? conf.label + ' · ' + parts.join(' · ') : conf.label;
|
||||||
|
} else {
|
||||||
|
el.textContent = conf.label;
|
||||||
|
}
|
||||||
|
el.style.backgroundColor = conf.bg;
|
||||||
|
// 呼吸仅限 building;离开时置 none,状态间观感交给 background-color 的 200ms 过渡
|
||||||
|
el.style.animation = data.state === 'building'
|
||||||
|
? 'deploy-status-breath 1.6s ease-in-out infinite'
|
||||||
|
: 'none';
|
||||||
|
if (data.state === 'fail') {
|
||||||
|
el.style.pointerEvents = 'auto';
|
||||||
|
el.style.cursor = 'pointer';
|
||||||
|
el.title = '点击刷新页面'; // fail 的唯一动作是刷新:取回最新静态产物(含本文件与状态 JSON)
|
||||||
|
} else {
|
||||||
|
el.style.pointerEvents = 'none';
|
||||||
|
el.style.cursor = 'default';
|
||||||
|
el.title = '';
|
||||||
|
}
|
||||||
|
el.style.opacity = '1';
|
||||||
|
}
|
||||||
|
|
||||||
|
function poll() {
|
||||||
|
fetch('/admin/build-status.json', { cache: 'no-store' }) // no-store:状态文件必须每次拿实况
|
||||||
|
.then(function (r) { return r.ok ? r.json() : null; }) // 404 等非 200 → 当不可知处理
|
||||||
|
.then(function (data) { render(data && typeof data === 'object' ? data : {}); })
|
||||||
|
.catch(function () { hide(); }); // 网络层与 JSON 解析失败同路:淡出,下一轮再试
|
||||||
|
}
|
||||||
|
|
||||||
|
function loop() {
|
||||||
|
poll();
|
||||||
|
timer = setTimeout(loop, POLL_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('visibilitychange', function () {
|
||||||
|
if (document.visibilityState === 'visible') {
|
||||||
|
// 回来立即拉(隐藏期间可能已构建完成),并重置 10s 节拍
|
||||||
|
if (timer) clearTimeout(timer);
|
||||||
|
loop();
|
||||||
|
} else if (timer) {
|
||||||
|
// 隐藏即停:省请求,也避免后台标签被浏览器节流的定时器醒来后集中补发
|
||||||
|
clearTimeout(timer);
|
||||||
|
timer = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
window.DEPLOY_STATUS = {
|
||||||
|
// 挂载即去重标记;stop 供控制台/测试卸载(页面自身是 SPA,正常导航不会触发)
|
||||||
|
stop: function () {
|
||||||
|
if (timer) clearTimeout(timer);
|
||||||
|
timer = null;
|
||||||
|
el.remove();
|
||||||
|
var style = document.getElementById('deploy-status-style');
|
||||||
|
if (style) style.remove();
|
||||||
|
delete window.DEPLOY_STATUS;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (document.visibilityState === 'visible') loop();
|
||||||
|
// 初始就隐藏(预渲染标签页):等 visibilitychange 回来再首拉
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.body) boot();
|
||||||
|
else document.addEventListener('DOMContentLoaded', boot); // 若被放到 <head> 且未加 defer
|
||||||
|
})();
|
||||||
@@ -3,13 +3,74 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="robots" content="noindex" />
|
<meta name="robots" content="noindex" />
|
||||||
<title>Sveltia CMS</title>
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>wait4x 后台</title>
|
||||||
|
<!-- 站点图标 + PWA(5n B2:手机加主屏独立入口) -->
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
|
<link rel="manifest" href="./manifest.webmanifest" />
|
||||||
|
<style>
|
||||||
|
/* 速通坞(5n D1 + 用户实测防误触改版):默认收起成一枚小圆钮,
|
||||||
|
点开才展开五入口;开关状态存 localStorage(手机端默认必收起)。
|
||||||
|
收起态只有一个 26px 圆钮,不再与编辑区抢点击面。 */
|
||||||
|
#qd-toggle{position:fixed;top:10px;right:12px;z-index:9000;width:26px;height:26px;
|
||||||
|
border-radius:50%;border:1px solid rgba(127,131,140,.4);background:rgba(255,255,255,.85);
|
||||||
|
color:#5b6472;font:14px/1 system-ui,sans-serif;cursor:pointer;opacity:.6;transition:opacity .2s}
|
||||||
|
#qd-toggle:hover{opacity:1}
|
||||||
|
body[data-qd=open] #qd-toggle{background:rgba(52,84,209,.12);color:#3454d1;border-color:rgba(52,84,209,.4)}
|
||||||
|
#qd-links{position:fixed;top:44px;right:12px;z-index:9000;display:none;flex-direction:column;gap:4px;
|
||||||
|
font:12px/1 system-ui,sans-serif;padding:6px;border-radius:10px;background:rgba(255,255,255,.95);
|
||||||
|
box-shadow:0 2px 10px rgba(0,0,0,.12)}
|
||||||
|
body[data-qd=open] #qd-links{display:flex}
|
||||||
|
#qd-links a{color:#5b6472;text-decoration:none;padding:5px 10px;border-radius:6px;white-space:nowrap}
|
||||||
|
#qd-links a:hover{color:#3454d1;background:rgba(52,84,209,.08)}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<!-- pinned 本地 bundle(0.252.0):官方模板仅此一个 script,无 CSS link、无 type=module -->
|
<nav id="quick-dock" aria-label="快捷入口">
|
||||||
|
<button id="qd-toggle" type="button" aria-expanded="false" title="快捷入口">≡</button>
|
||||||
|
<div id="qd-links">
|
||||||
|
<a href="https://wait4x.online/artalk/" target="_blank" rel="noopener">评论管理</a>
|
||||||
|
<a href="./stats.html" target="_blank" rel="noopener">内容仪表盘</a>
|
||||||
|
<a href="https://git.wait4x.online/wait4x/blog" target="_blank" rel="noopener">仓库</a>
|
||||||
|
<a href="https://git.wait4x.online/wait4x/blog/src/branch/main/docs/%E5%90%8E%E5%8F%B0%E5%9B%9E%E6%BB%9A%E6%8C%87%E5%BC%95.md" target="_blank" rel="noopener">回滚指引</a>
|
||||||
|
<a href="https://git.wait4x.online/wait4x/blog/src/branch/main/docs/%E7%AE%A1%E7%90%86%E5%8F%B0.md" target="_blank" rel="noopener">管理台手册</a>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
<script>
|
||||||
|
// 速通坞开合:默认收起(误触根除);记住用户选择;点外部自动收起
|
||||||
|
(function () {
|
||||||
|
var t = document.getElementById('qd-toggle');
|
||||||
|
var set = function (open) {
|
||||||
|
document.body.dataset.qd = open ? 'open' : 'closed';
|
||||||
|
t.setAttribute('aria-expanded', String(open));
|
||||||
|
try { localStorage.setItem('qd-open', open ? '1' : '0'); } catch (e) {}
|
||||||
|
};
|
||||||
|
var saved;
|
||||||
|
try { saved = localStorage.getItem('qd-open'); } catch (e) {}
|
||||||
|
set(saved === '1'); // 缺省收起
|
||||||
|
t.addEventListener('click', function () {
|
||||||
|
set(document.body.dataset.qd !== 'open');
|
||||||
|
});
|
||||||
|
document.addEventListener('click', function (e) {
|
||||||
|
if (document.body.dataset.qd === 'open' && !e.target.closest('#quick-dock')) set(false);
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
<!-- pinned 本地 bundle(0.259.0,5n T1 升级):官方模板仅此一个 script,无 CSS link、无 type=module -->
|
||||||
<script src="./sveltia-cms.js"></script>
|
<script src="./sveltia-cms.js"></script>
|
||||||
<!-- 可视化排版选择:类清单(单一数据源)→ 编辑器组件注册(须在 CMS 之后、init 之前注册) -->
|
<!-- 艺术字体切片(unicode-range 按需取片):选择面板的示例字形用真实字体渲染 -->
|
||||||
|
<link rel="stylesheet" href="/fonts/subset/hwmc/fonts.css" />
|
||||||
|
<link rel="stylesheet" href="/fonts/xiaowei/fonts.css" />
|
||||||
|
<link rel="stylesheet" href="/fonts/huangyou/fonts.css" />
|
||||||
|
<link rel="stylesheet" href="/fonts/mashanzheng/fonts.css" />
|
||||||
|
<link rel="stylesheet" href="/fonts/liujianmaocao/fonts.css" />
|
||||||
|
<link rel="stylesheet" href="/fonts/smiley/fonts.css" />
|
||||||
|
<link rel="stylesheet" href="/fonts/neoxihei/fonts.css" />
|
||||||
|
<!-- 可视化排版选择:类清单(单一数据源)→ 编辑器组件注册 → 预览样式注入(顺序固定:CMS 之后、init 之前) -->
|
||||||
<script src="./typography-classes.js"></script>
|
<script src="./typography-classes.js"></script>
|
||||||
<script src="./typography-editor.js"></script>
|
<script src="./typography-editor.js"></script>
|
||||||
|
<script src="./preview-style.js"></script>
|
||||||
|
<!-- 部署状态条(5n B1):轮询 /admin/build-status.json -->
|
||||||
|
<script src="./deploy-status.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"name": "wait4x 后台",
|
||||||
|
"short_name": "后台",
|
||||||
|
"start_url": "/admin/",
|
||||||
|
"scope": "/admin/",
|
||||||
|
"display": "standalone",
|
||||||
|
"background_color": "#ffffff",
|
||||||
|
"theme_color": "#3454d1",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "/icons/icon-192.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/icons/icon-512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
/**
|
||||||
|
* 后台预览一致性(5l T1 / 5n T7):把站点排版效果注入 Sveltia 预览窗格。
|
||||||
|
*
|
||||||
|
* 现状问题:markdown 里的排版类(font- 字体、c-/ink- 颜色)在预览里是素文本——
|
||||||
|
* 类名随 DOM 保留(typography-editor 的 toPreview 输出带 class),但预览
|
||||||
|
* 文档没有站点 CSS,颜色字体全部不可见。
|
||||||
|
*
|
||||||
|
* 方案(官方口子,勿改成 iframe hack——见 spec 5l §2.5):
|
||||||
|
* 1. 基础富文本排版 + 全部排版类 CSS 从 window.TYPOGRAPHY_CLASSES
|
||||||
|
* **运行时生成**(清单单一事实源:typography-classes.js 改一处,
|
||||||
|
* 选择面板与预览自动同步),raw 模式注入;
|
||||||
|
* 2. 自托管艺术字体挂切片 CSS(public/fonts/<family>/fonts.css,
|
||||||
|
* unicode-range 按需取片——预览里只拉用到的字所落在的切片,不整包)。
|
||||||
|
*
|
||||||
|
* 暗色(5n T7):所有色值走 CSS 自定义属性,变量缺省值 = 亮态(与改造前同一批
|
||||||
|
* 字面量,无暗色命中时逐字节等价),末尾一段 @media (prefers-color-scheme: dark)
|
||||||
|
* 统一换档——后台切暗色时预览不再用亮态色值配暗底。
|
||||||
|
*
|
||||||
|
* serif/kai/kuaile 的 CSS 经 Vite ?url 打包成哈希名,后台引不到稳定 URL,
|
||||||
|
* 预览走字体栈里的系统回退(Kaiti SC/Songti)——近似可见,文档已注明。
|
||||||
|
* 加载顺序:须在 typography-classes.js 之后、CMS init 之前(index.html 固定序)。
|
||||||
|
*/
|
||||||
|
(function () {
|
||||||
|
const CLASSES = window.TYPOGRAPHY_CLASSES;
|
||||||
|
if (!CLASSES || !window.CMS || !window.CMS.registerPreviewStyle) return;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 基础排版的暗态值(亮态值不在这里登记,直接作各条 var() 的回落值)。
|
||||||
|
* 出处:src/styles/tokens.css `[data-theme="dark"]` 段,勿凭观感改。
|
||||||
|
*/
|
||||||
|
const BASE_DARK = {
|
||||||
|
'pv-accent': '#8ea2ff', // --accent
|
||||||
|
'pv-line': '#262b33', // --line(站点暗色分隔线就是这个档)
|
||||||
|
'pv-quote-ink': '#8b8f98', // --ink-3
|
||||||
|
'pv-code-bg': '#1f242b', // --bg-soft
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 颜色类暗态值,键 = typography-classes.js 里的 cls。同样取自暗色 tokens 段:
|
||||||
|
* 站点语义色板暗态是独立取值(不是亮态加亮),硬套亮态在 #191d23 底上不达 AA。
|
||||||
|
*/
|
||||||
|
const CLASS_DARK = {
|
||||||
|
'ink-1': '#f5f5f7', // --ink-1
|
||||||
|
'ink-2': '#c9ccd4', // --ink-2
|
||||||
|
'ink-3': '#8b8f98', // --ink-3
|
||||||
|
// 站点 CSS 里 ink-muted 映射 --ink-4,而暗态 --ink-4 (#5c6066) 是装饰档,
|
||||||
|
// 暗底上近乎不可见;亮态 swatch 本就取的是 --ink-faint 的值,暗态随之一致
|
||||||
|
'ink-muted': '#84888f', // --ink-faint
|
||||||
|
'ink-warm': '#d6cbbd', // --ink-warm
|
||||||
|
'ink-cool': '#b9c6d6', // --ink-cool
|
||||||
|
'ink-faint': '#84888f', // --ink-faint
|
||||||
|
'ink-accent': '#8ea2ff', // --accent
|
||||||
|
'ink-accent-soft': '#a8b7f7', // --accent-soft-text
|
||||||
|
'c-blue': '#7fb0ff',
|
||||||
|
'c-blue-soft': '#3381ff',
|
||||||
|
'c-teal': '#6fd6c8',
|
||||||
|
'c-teal-soft': '#2a9384',
|
||||||
|
'c-green': '#7ddf99',
|
||||||
|
'c-green-soft': '#269a47',
|
||||||
|
'c-yellow': '#ffd166',
|
||||||
|
'c-yellow-soft': '#af7a00',
|
||||||
|
'c-orange': '#ffb163',
|
||||||
|
'c-orange-soft': '#d16800',
|
||||||
|
'c-red': '#ff8b8b',
|
||||||
|
'c-red-soft': '#ff2828',
|
||||||
|
'c-pink': '#ff9ecb',
|
||||||
|
'c-pink-soft': '#ff0d7e',
|
||||||
|
'c-purple': '#c4a6ff',
|
||||||
|
'c-purple-soft': '#9a67ff',
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 未登记暗态值的类(清单后续新增)用 color-mix 提亮近似——宁可偏亮,
|
||||||
|
* 也不要暗底上跑亮态深色(那是这次要修的问题本身)。
|
||||||
|
*/
|
||||||
|
const approxDark = (swatch) => `color-mix(in oklab, ${swatch} 82%, #fff)`;
|
||||||
|
|
||||||
|
/** 预览基调:近似站点 .rich 的通用排版(目标是类效果可见,非像素级还原) */
|
||||||
|
const baseCss = [
|
||||||
|
'p{margin:.6em 0;line-height:1.75}',
|
||||||
|
'h1,h2,h3,h4{font-weight:600;line-height:1.4;margin:1.2em 0 .5em}',
|
||||||
|
'h2{font-size:1.3em}h3{font-size:1.15em}',
|
||||||
|
'ul,ol{padding-left:1.5em;margin:.6em 0}li{margin:.25em 0}',
|
||||||
|
'a{color:var(--pv-accent,#3454d1);text-decoration:underline}',
|
||||||
|
'blockquote{margin:.8em 0;padding:.2em 1em;border-left:3px solid var(--pv-line,#e6e7eb);color:var(--pv-quote-ink,#6b6e78)}',
|
||||||
|
"code{font-family:'JetBrains Mono',ui-monospace,monospace;font-size:.92em;background:var(--pv-code-bg,#f2f3f5);padding:.1em .35em;border-radius:4px}",
|
||||||
|
'img{max-width:100%}',
|
||||||
|
'hr{border:none;border-top:1px solid var(--pv-line,#e6e7eb);margin:1.5em 0}',
|
||||||
|
'strong{font-weight:700}',
|
||||||
|
].join('');
|
||||||
|
|
||||||
|
/** 排版类规则:清单驱动(swatch/family 亮态值作 var() 回落 = 面板所见) */
|
||||||
|
const rules = [];
|
||||||
|
const darkDecls = [];
|
||||||
|
for (const f of CLASSES.font || []) {
|
||||||
|
if (f.family) rules.push(`.${f.cls}{font-family:${f.family}}`);
|
||||||
|
}
|
||||||
|
for (const c of CLASSES.color || []) {
|
||||||
|
if (!c.swatch) continue;
|
||||||
|
const name = `--pv-${c.cls}`;
|
||||||
|
rules.push(`.${c.cls}{color:var(${name},${c.swatch})}`);
|
||||||
|
darkDecls.push(`${name}:${CLASS_DARK[c.cls] || approxDark(c.swatch)}`);
|
||||||
|
}
|
||||||
|
for (const [name, value] of Object.entries(BASE_DARK)) darkDecls.push(`--${name}:${value}`);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 为什么用媒体查询而不是监听「后台切主题」:Sveltia 的 registerPreviewStyle 是
|
||||||
|
* 一次性注入(无更新/移除接口),registerEventListener 只支持 pre/postSave 与
|
||||||
|
* pre/postPublish 这类保存事件,没有任何主题切换回调;后台主题默认跟随系统,
|
||||||
|
* 预览文档的 prefers-color-scheme 与后台同源,浏览器自己换档即可,零重注入。
|
||||||
|
* 代价:用户在后台显式选 light 而系统是 dark 时预览偏暗——已知取舍,比监听
|
||||||
|
* Sveltia 内部 DOM(非公开契约)稳妥。
|
||||||
|
*/
|
||||||
|
const darkCss = darkDecls.length ? `@media (prefers-color-scheme: dark){:root{${darkDecls.join(';')}}}` : '';
|
||||||
|
|
||||||
|
window.CMS.registerPreviewStyle(baseCss + rules.join('') + darkCss, { raw: true });
|
||||||
|
|
||||||
|
// 字体切片(URL 模式):预览文档自己的 @font-face;去重后逐个挂
|
||||||
|
const seen = new Set();
|
||||||
|
for (const f of CLASSES.font || []) {
|
||||||
|
if (!f.css || seen.has(f.css)) continue;
|
||||||
|
seen.add(f.css);
|
||||||
|
window.CMS.registerPreviewStyle(f.css);
|
||||||
|
}
|
||||||
|
})();
|
||||||
@@ -5,6 +5,9 @@
|
|||||||
*
|
*
|
||||||
* swatch / family 仅供**后台面板预览**使用(后台无站点 CSS 变量,故给具体值):
|
* swatch / family 仅供**后台面板预览**使用(后台无站点 CSS 变量,故给具体值):
|
||||||
* swatch 取亮态值(后台是浅色 UI),family 为该类的真实字体栈。
|
* swatch 取亮态值(后台是浅色 UI),family 为该类的真实字体栈。
|
||||||
|
* css = 该类自托管的切片 CSS(public/fonts/<family>/fonts.css,unicode-range
|
||||||
|
* 按需取片);选择面板与预览窗格都会挂它,让字形真实可见。serif/kai/kuaile
|
||||||
|
* 走 Vite ?url 哈希名引不到稳定 URL,不给 css 字段(系统回退近似)。
|
||||||
*/
|
*/
|
||||||
window.TYPOGRAPHY_CLASSES = {
|
window.TYPOGRAPHY_CLASSES = {
|
||||||
font: [
|
font: [
|
||||||
@@ -16,8 +19,16 @@ window.TYPOGRAPHY_CLASSES = {
|
|||||||
{ cls: 'font-kai-sys', label: '楷体族', family: '"LXGW WenKai Screen", "Kaiti SC", STKaiti, 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-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-fangsong', label: '仿宋族', family: 'FangSong, STFangsong, serif', kind: '系统回退' },
|
||||||
{ cls: 'font-huiwen', label: '汇文明朝体', family: '"WF-Hwmc", serif', kind: '自托管' },
|
{ cls: 'font-huiwen', css: '/fonts/subset/hwmc/fonts.css', label: '汇文明朝体', family: '"WF-Hwmc", serif', kind: '自托管' },
|
||||||
{ cls: 'font-kuaile', label: '站酷快乐体', family: '"ZCOOL KuaiLe", 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', css: '/fonts/xiaowei/fonts.css', label: '站酷小薇(文学·宋)', family: '"ZCOOL XiaoWei", serif', kind: '自托管' },
|
||||||
|
{ cls: 'font-huangyou', css: '/fonts/huangyou/fonts.css', label: '站酷庆科黄油(标题)', family: '"ZCOOL QingKe HuangYou", sans-serif', kind: '自托管' },
|
||||||
|
{ cls: 'font-mashanzheng', css: '/fonts/mashanzheng/fonts.css', label: '马善政(书法)', family: '"Ma Shan Zheng", "Kaiti SC", serif', kind: '自托管' },
|
||||||
|
{ cls: 'font-liujianmaocao', css: '/fonts/liujianmaocao/fonts.css', label: '柳建毛草(草书)', family: '"Liu Jian Mao Cao", "Kaiti SC", cursive', kind: '自托管' },
|
||||||
|
{ cls: 'font-smiley', css: '/fonts/smiley/fonts.css', label: '得意黑(斜体标题)', family: '"Smiley Sans", sans-serif', kind: '自托管' },
|
||||||
|
{ cls: 'font-neoxihei', css: '/fonts/neoxihei/fonts.css', label: '霞鹜新晰黑(正文黑)', family: '"LXGW Neo XiHei", "Noto Sans SC", sans-serif', kind: '自托管' },
|
||||||
],
|
],
|
||||||
color: [
|
color: [
|
||||||
{ cls: 'ink-1', label: '最重(标题级)', swatch: '#111113' },
|
{ cls: 'ink-1', label: '最重(标题级)', swatch: '#111113' },
|
||||||
|
|||||||
@@ -133,7 +133,7 @@
|
|||||||
label: '排版(字体/颜色)',
|
label: '排版(字体/颜色)',
|
||||||
icon: 'format_paint',
|
icon: 'format_paint',
|
||||||
type: 'inline',
|
type: 'inline',
|
||||||
pattern: /<span class="(?<cls>[a-z0-9-]+)">(?<text>[^<]*?)<\/span>/,
|
pattern: /<span class="(?<cls>[a-z0-9- ]+)">(?<text>[^<]*?)<\/span>/, // 多类空格分隔(font-kai c-red)也匹配
|
||||||
toInline: ({ cls = '', text = '' }) => `<span class="${cls}">${text}</span>`,
|
toInline: ({ cls = '', text = '' }) => `<span class="${cls}">${text}</span>`,
|
||||||
toPreview: ({ cls = '', text = '' }) => {
|
toPreview: ({ cls = '', text = '' }) => {
|
||||||
const span = document.createElement('span');
|
const span = document.createElement('span');
|
||||||
|
|||||||