docs(admin): 后台文档与 hint 补齐 + 修复 config.yml 重复键结构缺陷(5g T6)

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

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

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

Co-authored-by: GLM-5.3
This commit is contained in:
wait4xandGLM-5.3 committed 2026-11-02 19:16:43 +08:00
1 parent 3e09a362cc
commit 8274180f81
6 files changed
+304 -12

No files matched your search

+102
View File
@@ -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())