Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c772236d1a | ||
|
|
a89c443c25 | ||
|
|
23b79a2473 | ||
|
|
8274180f81 |
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"}
|
||||
+14
-14
@@ -34,13 +34,13 @@
|
||||
## 3. 功能范围
|
||||
|
||||
### 3.1 核心功能(MVP)
|
||||
- [ ] T1 blocks 骨架:schema(discriminatedUnion)+ page-blocks.ts + BlockRenderer + prose/tracks/divider 三块 + 后台 types 声明
|
||||
- [ ] T2 块型扩充:cards/timeline/quote/figure/linklist/cta + 图标键白名单 + 图片 image() 管线
|
||||
- [ ] T3 样式即数据:theme.yaml + theme.ts + BaseLayout 内联变量 + 对比度闸门脚本
|
||||
- [ ] T4 首页细化与去重:抽 HomePage.astro 消 zh/en 双份 + 字段细化(projectsLimit/announcement 等)
|
||||
- [ ] T5 开源页收尾:README 缓存策略/超时/回落矩阵 + fallback JSON 回填 + include 未命中 WARN
|
||||
- [ ] T6 文档与 hint:docs/管理台.md(块编辑用法)+ 写作指南扩充 + admin 全字段中文 hint
|
||||
- [ ] T7 字体子集化工具:按页用字裁剪艺术字体为小切片 + build.sh 守卫挂载 + 实测对比
|
||||
- [x] T1 blocks 骨架:schema(discriminatedUnion)+ page-blocks.ts + BlockRenderer + prose/tracks/divider 三块 + 后台 types 声明
|
||||
- [x] T2 块型扩充:cards/timeline/quote/figure/linklist/cta + 图标键白名单 + 图片 image() 管线
|
||||
- [x] T3 样式即数据:theme.yaml + theme.ts + BaseLayout 内联变量 + 对比度闸门脚本
|
||||
- [x] T4 首页细化与去重:抽 HomePage.astro 消 zh/en 双份 + 字段细化(projectsLimit/announcement 等)
|
||||
- [x] T5 开源页收尾:README 缓存策略/超时/回落矩阵 + fallback JSON 回填 + include 未命中 WARN
|
||||
- [x] T6 文档与 hint:docs/管理台.md(块编辑用法)+ 写作指南扩充 + admin 全字段中文 hint
|
||||
- [x] T7 字体子集化工具:按页用字裁剪艺术字体为小切片 + build.sh 守卫挂载 + 实测对比
|
||||
|
||||
### 3.2 扩展功能
|
||||
- 无
|
||||
@@ -69,13 +69,13 @@
|
||||
- 升级触发:若 theme 变量波及全局排版(字体字号全站变化)或 blocks 改动触及首页/文章页结构,升级为全站视觉复核与 E2E 全量并记录理由
|
||||
|
||||
## 6. 成功标准与验证方式
|
||||
- [ ] 关于页整页由 yaml blocks 驱动,增删排序生效 -> verify: task_0001
|
||||
- [ ] 九类块全部可渲染且旧内容零回归(三级回落) -> verify: task_0002
|
||||
- [ ] theme.yaml 改值 → 产物 CSS 变量变化,对比度闸门拦截不达标 -> verify: task_0003
|
||||
- [ ] 首页 zh/en 由同一组件渲染且字段可配 -> verify: task_0004
|
||||
- [ ] 开源页 README 失败矩阵与 fallback 告警可用 -> verify: task_0005
|
||||
- [ ] 后台文档与 hint 完整 -> verify: task_0006
|
||||
- [ ] 字体子集化工具把指定字体单页增量压到 ≤150KB -> verify: task_0007
|
||||
- [x] 关于页整页由 yaml blocks 驱动,增删排序生效 -> verify: task_0001
|
||||
- [x] 九类块全部可渲染且旧内容零回归(三级回落) -> verify: task_0002
|
||||
- [x] theme.yaml 改值 → 产物 CSS 变量变化,对比度闸门拦截不达标 -> verify: task_0003
|
||||
- [x] 首页 zh/en 由同一组件渲染且字段可配 -> verify: task_0004
|
||||
- [x] 开源页 README 失败矩阵与 fallback 告警可用 -> verify: task_0005
|
||||
- [x] 后台文档与 hint 完整 -> verify: task_0006
|
||||
- [x] 字体子集化工具把指定字体单页增量压到 ≤150KB -> verify: task_0007
|
||||
|
||||
## 7. 风险与约束
|
||||
- 灰阶体系与双语红线不变;彩色仅正文强调
|
||||
+2
-2
@@ -21,7 +21,7 @@
|
||||
- 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`
|
||||
|
||||
- [ ] T4 首页细化与去重
|
||||
- [x] T4 首页细化与去重
|
||||
- claim: op-artalk
|
||||
- boundary: 新增 src/components/pages/HomePage.astro(承接现 index.astro 区块逻辑,locale prop)、src/pages/{index,en/index}.astro 改为薄壳、home.yaml 增 projectsLimit / announcement(markdown)/ postsTracks(轨开关细分),site-content 访问器同步
|
||||
- verify: `E2E_BASE=http://127.0.0.1:18924 node tests/e2e.mjs 2>&1 | grep -q "37/37 PASS" && test -f src/components/pages/HomePage.astro && grep -q "HomePage" src/pages/en/index.astro`
|
||||
@@ -31,7 +31,7 @@
|
||||
- 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"`
|
||||
|
||||
- [ ] T6 文档与后台 hint 补齐
|
||||
- [x] T6 文档与后台 hint 补齐
|
||||
- claim: worker-one
|
||||
- boundary: docs/管理台.md(块编辑怎么用、增删排序、预览限制)、docs/写作指南.md(块字段表/受限类/主题项)、public/admin/config.yml 全字段中文 hint(含 T1-T4 新增字段)
|
||||
- verify: `test -f docs/管理台.md && grep -q "blocks\|区块" docs/管理台.md && grep -c "hint" public/admin/config.yml | awk '$1>=20'`
|
||||
+44
-1
@@ -153,6 +153,18 @@ https://astro.build/
|
||||
|
||||
体积账结论(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 指向子集)是独立一步,接线后随构建自动生成,未接线前需手动跑。**换字/加字后重跑一次**,否则新字会回落到下一个切片或系统字体。
|
||||
|
||||
### 候补字体清单(未落地,加一款约"一条命令 + 一次体积账")
|
||||
|
||||
| 字体 | 许可证 | 允许 webfont 嵌入 | 未落地原因 |
|
||||
@@ -168,7 +180,7 @@ https://astro.build/
|
||||
| 阿里妈妈东方大楷 / 演示悠然小楷 | 免费商用 / 个人免费 | ⚠️ 需逐款核嵌入条款 | 无包;"个人免费"字体常禁 @font-face |
|
||||
| 霞鹜文楷 Light/Bright | OFL-1.1 | ✅ | 实测 **+8605KB**(CJK 切片粒度所致) |
|
||||
|
||||
> 加新字体的流程:①核许可证与 **web 嵌入条款**(很多"个人免费"字体明文禁 @font-face,只能进系统回退)②npm 装或放入 `public/fonts/<family>/`(优选 cn-font-split 类切片包)③`global.css` 加类 + `rehype-class-whitelist` 放行 + `public/admin/typography-classes.js` 加一项 ④跑体积账,超 150KB/页 的要评估是否值得。
|
||||
> 加新字体的流程:①核许可证与 **web 嵌入条款**(很多"个人免费"字体明文禁 @font-face,只能进系统回退)②npm 装或放入 `public/fonts/<family>/`(优选 cn-font-split 类切片包)③`global.css` 加类 + `rehype-class-whitelist` 放行 + `public/admin/typography-classes.js` 加一项 ④跑体积账,超 150KB/页 的要评估是否值得;展示类字体再跑一次 `scripts/build-perf/subset-fonts.py` 按实际用字裁切(命令见上节)。
|
||||
|
||||
```markdown
|
||||
<span class="ink-muted">次要说明</span>
|
||||
@@ -211,6 +223,34 @@ typography:
|
||||
|
||||
**层 3 · 艺术字体**:`font-art-kuaile`(站酷快乐体,艺术字专用类)。⚠️ **仅用于标题/短句**:艺术字字形大,CJK 切片体积高(实测用到页 +163KB),长段使用会显著抬高单页字体成本。
|
||||
|
||||
## 页面块(关于页 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 秒)
|
||||
@@ -303,6 +343,9 @@ markdown 原生语法,无需任何配置:
|
||||
| `latestPhotos` | 0-6 | 4 | 「最新影像」张数;**0 = 隐藏该区块** |
|
||||
| `perTrackLimit` | 4-8 | 6 | 首页阅读/技术两轨各显示条数 |
|
||||
| `sections` | `{tracks, photos, heatmap, projects}` 布尔 | 全开 | 区块开关,关掉即不渲染 |
|
||||
| `projectsLimit` | 1-99 | 6 | 首页项目墙条数(越界夹取) |
|
||||
| `announcement` | `{zh, en}`(markdown 富文本) | 空 | 名片区下方公告;空 = 不渲染 |
|
||||
| `postsTracks` | `{reading, tech, photo}` 布尔 | 全开 | 关掉某轨则该轨文章不在首页列出(归档页仍可见) |
|
||||
|
||||
越界值会被夹取(如 perTrackLimit=20 按 8 处理),不报错;**全部留空 = 现有形态不变**。
|
||||
|
||||
|
||||
+131
@@ -1,17 +1,148 @@
|
||||
# 管理台(Sveltia CMS)使用指南
|
||||
|
||||
> 地址:`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,下次免登。
|
||||
|
||||
## 四类内容集合
|
||||
|
||||
| 入口 | 文件位置 | 用来放什么 | 关键约束 |
|
||||
|---|---|---|---|
|
||||
| 文章 | `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 秒自动构建上线。
|
||||
|
||||
## 照片上传流
|
||||
|
||||
1. 图库照片 → New Entry:填标题(**必填**,空标题会让构建炸——实测坑)、选分组(street/landscape)、可写 alt
|
||||
2. 图片字段 → 资产库上传 jpg(与 sidecar 同目录落位,零重压缩)
|
||||
3. Save → 自动 commit + 构建 → 画廊出现(EXIF 构建期自动提取,exif 字段留空即可)
|
||||
|
||||
## 块编辑:关于页的块式页面构建器
|
||||
|
||||
关于页(`content/site/about.yaml`)的正文由 **blocks 列表**构成,后台里是一串可拖拽排序的卡片。每个块都有「显示」开关;关掉只是不渲染,块本身留在数据里。
|
||||
|
||||
两条渲染纪律:
|
||||
|
||||
- 列表为空(或全关)时,页面回落到 `intro` 字段的 markdown 正文;`intro` 也空则用内置默认文案。三级回落,任何一级为空都不会出白页。
|
||||
- 未知块型(手工编辑 yaml 写错的 `type`)会被**跳过并打印 `[blocks]` 警告**,不会炸构建;块数上限 24,单块文本上限 4000 字,超限截断并 WARN。
|
||||
|
||||
九类块与字段(文本字段一律 `{zh, en}` 双语对象):
|
||||
|
||||
| 块型 | 必填 | 可选 | 用途 |
|
||||
|---|---|---|---|
|
||||
| `prose` 正文 | `body` | `title`、`width`(`narrow` 版心 / `full` 通栏) | markdown 长文,最常用 |
|
||||
| `tracks` 版图 | `title`、`items[].name` | `items[].icon`、`items[].desc` | 几条主线 / 能力面,图标白名单外或留空显示首字占位 |
|
||||
| `cards` 卡片墙 | `items[].title` | `title`、`columns`(2 或 3,默认 3)、`items[].desc/href/tag` | 并列展示若干条目 |
|
||||
| `timeline` 时间线 | `items[].time`、`items[].title` | `title`、`items[].desc` | 时间列是自由文本(`2024 — 2026` 也行) |
|
||||
| `quote` 引文 | `text` | `cite` | 单句金句,配 `font-kai` 更出效果 |
|
||||
| `figure` 图片 | `image` | `alt`、`caption` | **只能后台上传**(落 `content/site/assets/`,走图片管线),禁止外链 |
|
||||
| `linklist` 链接清单 | `items[].label`、`items[].href` | `title`、`items[].note` | 相关阅读 / 外部资源 |
|
||||
| `cta` 行动按钮 | `text`、`href`、`label` | — | 一页建议不超过一个 |
|
||||
| `divider` 分隔 | — | `style`(`line` 细线 / `space` 纯留白) | 长页分段 |
|
||||
|
||||
yaml 形态示例(手工编辑时照此写):
|
||||
|
||||
```yaml
|
||||
blocks:
|
||||
- type: prose
|
||||
title: { zh: 关于这个站点, en: About this site }
|
||||
body:
|
||||
zh: 这里写 markdown 正文,支持列表、粗斜体、链接与排版类。
|
||||
en: Markdown body here.
|
||||
width: narrow
|
||||
- type: quote
|
||||
text: { zh: 所谓暮色,就是光慢慢地承认自己要走。, en: Dusk is light admitting it has to go. }
|
||||
cite: { zh: 本站, en: this site }
|
||||
- type: figure
|
||||
image: ./assets/desk.jpg # 后台会替你写成这个相对路径
|
||||
caption: { zh: 工作台一角, en: A corner of the desk }
|
||||
```
|
||||
|
||||
其它可用字段:`layout.showIdCard`(关掉则不渲染名片区,默认开)、`typography`(见下一节)。
|
||||
|
||||
**预览限制**:后台只提供字段编辑与列表摘要(摘要显示块型名),块的真实排版以下次构建后的页面为准——想调间距 / 字号,请用「主题设置」而不是在块里硬凑空行。
|
||||
|
||||
## 页面排版覆盖(每页一处)
|
||||
|
||||
首页 / 关于页 / 开源页各自有一个「页面排版覆盖(可选)」对象:
|
||||
|
||||
| 字段 | 取值 | 说明 |
|
||||
|---|---|---|
|
||||
| `font` | 字族下拉(10 项) | 本页**正文**默认字体类 |
|
||||
| `accent` | 颜色下拉(18 项) | 本页强调色类 |
|
||||
| `scale` | 0.8–1.25 | 字号缩放,1 = 全站默认 |
|
||||
|
||||
- 只作用于**该页内容容器**(挂 `.page-typo` + CSS 变量),不写全站 tokens,其它页面零影响。
|
||||
- **体积成本提醒**:整页换艺术/展示字族(`font-huiwen` 汇文、`font-kuaile` 快乐体)意味着整页正文都要下载对应切片——单页字体成本会明显抬升,见写作指南的体积账。
|
||||
- 白名单外的类名会被忽略(构建期过滤),写错不报错、也不生效。
|
||||
|
||||
## 主题设置(全站 17 个旋钮)
|
||||
|
||||
「主题设置」入口写 `content/site/theme.yaml`,17 项全部可选,留空 = 仓库默认值。**配错不会炸站**:越界值回落默认,组内单调性被破坏则整组回落。
|
||||
|
||||
| 组 | 字段 | 范围 | 约束语义 |
|
||||
|---|---|---|---|
|
||||
| 强调色 | `accent_light` / `accent_dark` | `#rrggbb` | 明暗各一份;构建期对比度闸门断言正文 ≥4.5:1,不达标构建失败 |
|
||||
| 圆角 | `radius_img` | 4–16 | 图片圆角,建议 ≤14 |
|
||||
| 标题字号 | `text_h1` / `text_h2` / `text_h3` | h1/h2:22–32,h3:16–32 | **必须严格递减**(h1 > h2 > h3),否则三档一起回落默认 |
|
||||
| 正文字号 | `prose_size_reading` / `prose_size_tech` | 16–22 | 书评 / 技术文分轨独立 |
|
||||
| 行高 | `leading_reading` / `leading_tech` | 1.5–2.0 | 同上分轨 |
|
||||
| 间距梯度 | `space_3` / `space_4` / `space_5` / `space_6` | 8–80 | **必须严格递增**,否则整组回落 |
|
||||
| 流距 | `flow_p` / `flow_h2_post` | 0–2(em) | 段间距 / 标题下间距 |
|
||||
| 动效 | `dur` | 150–600(ms) | 过渡时长;`prefers-reduced-motion` 下自动归零 |
|
||||
|
||||
不能配的东西(有意为止):版心宽度、断点、纸墨色、代码配色——它们牵动版面结构与分轨身份,属于设计红线。注入方式是构建期内联 `:root:root{…}`,运行时零 JS、零请求。
|
||||
|
||||
## 首页与开源页的其余旋钮
|
||||
|
||||
### 首页三字段(5g 新增)
|
||||
|
||||
| 字段 | 取值 | 默认 | 作用 |
|
||||
|---|---|---|---|
|
||||
| `projectsLimit` | 1–99 | 6 | 首页项目墙条数(越界夹取) |
|
||||
| `announcement` | `{zh, en}` markdown | 空 | 名片区下方公告;**空 = 不渲染**,走富文本管线(可写排版类 span) |
|
||||
| `postsTracks` | `{reading, tech, photo}` 布尔 | 全开 | 关掉某轨,该轨文章不在首页列出(归档页仍可见) |
|
||||
|
||||
### 开源页(opensource.yaml)
|
||||
|
||||
| 字段 | 取值 | 说明 |
|
||||
|---|---|---|
|
||||
| `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 都不影响页面渲染;尺寸已由样式限制在内容宽度内。
|
||||
|
||||
## 纪律(与写作指南同源)
|
||||
|
||||
- **图片体积**:上传前自行压缩(长边 ≤2400px)——Sveltia 内建优化**刻意关闭**(会抹 EXIF 断构建期提取管线)
|
||||
|
||||
@@ -33,6 +33,7 @@ pnpm astro build
|
||||
# 按页字体 CSS 子集化(车道 A 任务 1):裁 @font-face 声明数,构建仓与本地同链;
|
||||
# 存在性守卫:新文件未推送到 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/config-check.py ] && python3 scripts/build-perf/config-check.py || echo "[build] config-check 未就绪,跳过"
|
||||
[ -f scripts/build-perf/contrast-check.mjs ] && node scripts/build-perf/contrast-check.mjs dist || echo "[build] contrast 脚本未就绪,跳过"
|
||||
# 首页阻塞 CSS 内联(车道 D):守卫先行同上——脚本未到 main 前绝不炸链
|
||||
[ -f scripts/build-perf/gen-sw-precache.mjs ] && node scripts/build-perf/gen-sw-precache.mjs dist || echo "[build] sw-precache 生成器未就绪,跳过"
|
||||
|
||||
+25
-10
@@ -142,6 +142,12 @@ collections:
|
||||
fields:
|
||||
- label: 身份语
|
||||
name: tagline
|
||||
widget: object
|
||||
required: false
|
||||
hint: 首页大字身份语(双语,markdown 富文本);可写 <span class="font-kai c-blue">…</span> 做局部强调
|
||||
fields:
|
||||
- { label: 中文, name: zh, widget: string, required: false }
|
||||
- { label: 英文, name: en, widget: string, required: false }
|
||||
- label: 页面排版覆盖(可选)
|
||||
name: typography
|
||||
widget: object
|
||||
@@ -152,17 +158,14 @@ collections:
|
||||
name: font
|
||||
widget: select
|
||||
required: false
|
||||
hint: 整页正文换字族;艺术/展示类字族(汇文、快乐体)体积大,单页成本会上升
|
||||
options: [font-serif, font-kai, font-sans, font-mono, font-song, font-kai-sys, font-hei-sys, font-fangsong, font-huiwen, font-kuaile]
|
||||
- label: 强调色类
|
||||
name: accent
|
||||
widget: select
|
||||
required: false
|
||||
options: [ink-accent, ink-accent-soft, c-blue, c-blue-soft, c-teal, c-teal-soft, c-green, c-green-soft, c-yellow, c-yellow-soft, c-orange, c-orange-soft, c-red, c-red-soft, c-pink, c-pink-soft, c-purple, c-purple-soft]
|
||||
- { label: 字号缩放, name: scale, widget: number, required: false, value_type: float, min: 0.8, max: 1.25, step: 0.05 }
|
||||
widget: object
|
||||
fields:
|
||||
- { label: 中文, name: zh, widget: string, required: false }
|
||||
- { label: 英文, name: en, widget: string, required: false }
|
||||
- { label: 字号缩放, name: scale, widget: number, required: false, value_type: float, min: 0.8, max: 1.25, step: 0.05, hint: '0.8–1.25;1 = 全站默认' }
|
||||
- label: 公告(可选,markdown)
|
||||
name: announcement
|
||||
widget: object
|
||||
@@ -256,6 +259,7 @@ collections:
|
||||
- label: 正文(markdown)
|
||||
name: prose
|
||||
widget: object
|
||||
hint: markdown 正文块;可选小标题与版心/通栏宽度,长文分段改内容用
|
||||
fields:
|
||||
- { label: 显示, name: enabled, widget: boolean, required: false, default: true }
|
||||
- label: 小标题(可选)
|
||||
@@ -280,6 +284,7 @@ collections:
|
||||
- label: 版图(图标+名称+描述)
|
||||
name: tracks
|
||||
widget: object
|
||||
hint: 版图块:图标 + 名称 + 描述,适合「几条主线/能力面」式介绍
|
||||
fields:
|
||||
- { label: 显示, name: enabled, widget: boolean, required: false, default: true }
|
||||
- label: 标题
|
||||
@@ -315,6 +320,7 @@ collections:
|
||||
- label: 卡片(2/3 列)
|
||||
name: cards
|
||||
widget: object
|
||||
hint: 卡片墙(2/3 列):每卡 = 标题 + 描述 + 可选链接/角标
|
||||
fields:
|
||||
- { label: 显示, name: enabled, widget: boolean, required: false, default: true }
|
||||
- label: 标题(可选)
|
||||
@@ -354,6 +360,7 @@ collections:
|
||||
- label: 时间线
|
||||
name: timeline
|
||||
widget: object
|
||||
hint: 时间线:一条时间 + 标题 + 描述;时间可写自由文本(如 2024 — 2026)
|
||||
fields:
|
||||
- { label: 显示, name: enabled, widget: boolean, required: false, default: true }
|
||||
- label: 标题(可选)
|
||||
@@ -385,6 +392,7 @@ collections:
|
||||
- label: 引文
|
||||
name: quote
|
||||
widget: object
|
||||
hint: 单句引文 + 可选署名;配 font-kai 排版效果更好
|
||||
fields:
|
||||
- { label: 显示, name: enabled, widget: boolean, required: false, default: true }
|
||||
- label: 引文
|
||||
@@ -403,6 +411,7 @@ collections:
|
||||
- label: 图片
|
||||
name: figure
|
||||
widget: object
|
||||
hint: 单图 + 可选说明;图片只能后台上传(落 content/site/assets),禁外链
|
||||
fields:
|
||||
- { label: 显示, name: enabled, widget: boolean, required: false, default: true }
|
||||
- { label: 图片(必填,请用后台上传), name: image, widget: image, hint: '禁止填外链:只能上传(落 content/site/assets/),走 Astro 图片管线' }
|
||||
@@ -417,6 +426,7 @@ collections:
|
||||
- label: 链接清单
|
||||
name: linklist
|
||||
widget: object
|
||||
hint: 链接清单(标签 + 链接 + 备注),适合「相关阅读 / 外部资源」
|
||||
fields:
|
||||
- { label: 显示, name: enabled, widget: boolean, required: false, default: true }
|
||||
- label: 标题(可选)
|
||||
@@ -448,6 +458,7 @@ collections:
|
||||
- label: 行动按钮
|
||||
name: cta
|
||||
widget: object
|
||||
hint: 行动按钮(文案 + 链接 + 按钮字);一页建议不超过一个
|
||||
fields:
|
||||
- { label: 显示, name: enabled, widget: boolean, required: false, default: true }
|
||||
- label: 说明文字
|
||||
@@ -466,6 +477,7 @@ collections:
|
||||
- label: 分隔
|
||||
name: divider
|
||||
widget: object
|
||||
hint: 分隔块:细分隔线或纯留白,给长页分段
|
||||
fields:
|
||||
- { label: 显示, name: enabled, widget: boolean, required: false, default: true }
|
||||
- label: 样式
|
||||
@@ -512,6 +524,12 @@ collections:
|
||||
fields:
|
||||
- label: 叙述(链接前)
|
||||
name: blurb
|
||||
widget: object
|
||||
required: false
|
||||
hint: 富文本(markdown + 排版类),渲染在仓库链接之前
|
||||
fields:
|
||||
- { label: 中文, name: zh, widget: string, required: false }
|
||||
- { label: 英文, name: en, widget: string, required: false }
|
||||
- label: 页面排版覆盖(可选)
|
||||
name: typography
|
||||
widget: object
|
||||
@@ -522,17 +540,14 @@ collections:
|
||||
name: font
|
||||
widget: select
|
||||
required: false
|
||||
hint: 整页正文换字族;艺术/展示类字族(汇文、快乐体)体积大,单页成本会上升
|
||||
options: [font-serif, font-kai, font-sans, font-mono, font-song, font-kai-sys, font-hei-sys, font-fangsong, font-huiwen, font-kuaile]
|
||||
- label: 强调色类
|
||||
name: accent
|
||||
widget: select
|
||||
required: false
|
||||
options: [ink-accent, ink-accent-soft, c-blue, c-blue-soft, c-teal, c-teal-soft, c-green, c-green-soft, c-yellow, c-yellow-soft, c-orange, c-orange-soft, c-red, c-red-soft, c-pink, c-pink-soft, c-purple, c-purple-soft]
|
||||
- { label: 字号缩放, name: scale, widget: number, required: false, value_type: float, min: 0.8, max: 1.25, step: 0.05 }
|
||||
widget: object
|
||||
fields:
|
||||
- { label: 中文, name: zh, widget: string, required: false }
|
||||
- { label: 英文, name: en, widget: string, required: false }
|
||||
- { label: 字号缩放, name: scale, widget: number, required: false, value_type: float, min: 0.8, max: 1.25, step: 0.05, hint: '0.8–1.25;1 = 全站默认' }
|
||||
- { label: 仓库链接地址, name: repo_url, widget: string, required: false }
|
||||
- { label: 仓库链接文字, name: repo_label, widget: string, required: false }
|
||||
- label: 叙述(链接后)
|
||||
|
||||
Executable
+102
@@ -0,0 +1,102 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
站点 YAML 配置结构校验(2026-11-02 教训驱动)
|
||||
|
||||
为什么需要:Python 默认 yaml.safe_load 对重复键是"后者覆盖前者"的静默行为,
|
||||
收编时用它验收会漏。本包实测 public/admin/config.yml 里 typography 被插进双语
|
||||
字段的 name: 与 widget: object 之间 → 同一 mapping 出现重复键 widget/fields,
|
||||
后台双语编辑器退化成字符串、排版选择器字段被劫持。本脚本用自定义构造器在
|
||||
解析阶段对重复键直接报错,并附带后台 hint 覆盖率体检。
|
||||
|
||||
用法:python3 scripts/build-perf/config-check.py [repo根=.]
|
||||
退出码:0 通过;1 有失败项
|
||||
"""
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
import yaml
|
||||
except ImportError:
|
||||
print('[config-check] 缺少 pyyaml:pip install pyyaml')
|
||||
sys.exit(1)
|
||||
|
||||
FILES = [
|
||||
'public/admin/config.yml',
|
||||
'content/site/home.yaml',
|
||||
'content/site/about.yaml',
|
||||
'content/site/opensource.yaml',
|
||||
'content/site/theme.yaml',
|
||||
]
|
||||
|
||||
|
||||
class StrictLoader(yaml.SafeLoader):
|
||||
"""对重复键报错的 SafeLoader(默认行为是静默覆盖)。"""
|
||||
|
||||
|
||||
def _construct_mapping(loader, node, deep=False):
|
||||
seen = set()
|
||||
for key_node, _value_node in node.value:
|
||||
key = loader.construct_object(key_node, deep=deep)
|
||||
if key in seen:
|
||||
raise yaml.YAMLError(
|
||||
f'duplicate key {key!r} at line {key_node.start_mark.line + 1}'
|
||||
)
|
||||
seen.add(key)
|
||||
return yaml.SafeLoader.construct_mapping(loader, node, deep=deep)
|
||||
|
||||
|
||||
StrictLoader.add_constructor(
|
||||
yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG, _construct_mapping
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
root = Path(sys.argv[1] if len(sys.argv) > 1 else '.')
|
||||
failed = 0
|
||||
loaded = {}
|
||||
|
||||
for rel in FILES:
|
||||
path = root / rel
|
||||
if not path.exists():
|
||||
print(f'[config-check] SKIP {rel}(不存在)')
|
||||
continue
|
||||
try:
|
||||
loaded[rel] = yaml.load(path.read_text(encoding='utf-8'), Loader=StrictLoader)
|
||||
print(f'[config-check] OK {rel}')
|
||||
except yaml.YAMLError as exc:
|
||||
failed += 1
|
||||
first = str(exc).splitlines()[0]
|
||||
print(f'[config-check] FAIL {rel}\n {first}')
|
||||
|
||||
admin = loaded.get('public/admin/config.yml')
|
||||
if admin:
|
||||
fields = hints = 0
|
||||
|
||||
def walk(node):
|
||||
nonlocal fields, hints
|
||||
if isinstance(node, dict):
|
||||
if 'name' in node:
|
||||
fields += 1
|
||||
if 'hint' in node:
|
||||
hints += 1
|
||||
for value in node.values():
|
||||
walk(value)
|
||||
elif isinstance(node, list):
|
||||
for value in node:
|
||||
walk(value)
|
||||
|
||||
walk(admin)
|
||||
print(f'[config-check] 后台字段 {fields} 个,其中带 hint {hints} 个')
|
||||
if hints < 20:
|
||||
failed += 1
|
||||
print('[config-check] FAIL hint 覆盖不足(<20),后台字段缺少中文说明')
|
||||
|
||||
if failed:
|
||||
print(f'[config-check] {failed} 项未通过')
|
||||
return 1
|
||||
print('[config-check] 全部通过')
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
raise SystemExit(main())
|
||||
Reference in new issue
Block a user