去共化

Opus 架构阶段 · 落地清单

配套 UPGRADE_PLAN.md。这是动代码前给用户过目的清单,不是已完成记录。 勾选规则:[ ] 待办 / [~] 进行中 / [x] 完成。改动应小步 git 提交(news-site-test 已有 git)。

最后更新:2026-07-04 · 状态:分支 feat/architecture-upgradeA(骨架)、C2/D2/D3/D4/D6/D8、F(主题脚手架)、B slice 1(配置驱动首页板块)完成,均构建通过、B slice 2(分类进导航下拉)、taxonomy-tags、C1 去重收敛、E slice 1(逐篇翻译骨架)、D6/D7 收尾、E8 canonicalOpus 架构阶段基本完成;Fable 视觉皮肤(theme① 定稿 + 组件精修 + H7/H8)已交付 2026-07-04。 剩:E slice 2(站点级 EN 切换 + chrome i18n,需定英文措辞 + 配合 JSM)、JSM 侧集成(E6:注入 block id / 产出翻译 JSON / home_blocks 编写 / 社媒 feed)、H3 CORS 确认、H6 分页页瘦身。


续接指南(新 session 如何无缝接手)

  • 分支feat/architecture-upgrademaster = 升级前基线还原点)。工作区应干净;git log --oneline 看 28+ 提交,每个提交信息都写清了做了什么 + 验证结果。
  • 构建/验证:仓库根 bundle exec jekyll build(产物 _site/)。本 session 全程”改 → build → grep 比对基线”验证(标签/分类计数、单一 html/body/title、feed URL 等),未破坏任何输出。
  • 文档三件套:本清单(逐项状态 + commit 号)、UPGRADE_PLAN.md(决策 / 板块 v4 / schema / 决策记录)、JSM_CONTRACTS.md(JSM 侧对接契约 + 行动清单)。三份都在 git 里。
  • 当前:Opus 架构阶段基本完成。剩 E slice 2(站点级 EN 切换 + chrome ui-strings,需定英文)+ JSM 侧集成 + Fable 视觉皮肤。
  • 注意:文章内容/版本由 JSM/用户在 JSM 里管,工作区可能出现内容文件增删(如某次 -v6 删除是用户有意为之)——不要擅自提交别人的内容改动,发现异常先 surface 给用户。

A. 布局骨架(修”布局体系倒置”)

  • A1 单一 _layouts/default.html:完整 <html>/<head>/<body> 骨架(wrapper+header+footer+共用脚本)+ `<main>

    JSM ↔ news-site-test 对接契约

面向 JSM 开发。news-site-test(Jekyll,站名「去共化」)负责渲染;JSM 负责写数据 / 生成产物 / 协调子站。本文件逐条列出 Jekyll 侧现在依赖或期待 JSM 提供的契约:格式、位置、当前状态、以及 JSM 需要做什么

配套:UPGRADE_PLAN.md(总计划)、OPUS_CHECKLIST.md(Jekyll 侧进度)。 分支:feat/architecture-upgrade。最后更新:2026-07-03。


契约总览

# 契约 位置 谁写 Jekyll 侧状态 JSM 侧状态
1 首页板块 home_blocks.yml _data/home_blocks.yml(仓库内) JSM ✅ 渲染器已就绪 ⬜ 待接管写入
2 子站 Latest Posts JSON 各子站的公开 URL JSM(latest_posts.py ✅ 通用组件已就绪 ✅ 三个首页 feed 均已产出
3 逐篇翻译(i18n) 文章 front matter + assets/i18n/*.json + 正文 data-i18n JSM ✅ 前端骨架已就绪 ⬜ 待做(最大一块)
4 Taxonomy 词条文件 _tag/*.md_category/*.md JSM ✅ 消费中 ✅ 已在管
5 文章版本身份 文章 front matter JSM ✅ 消费中 ✅ 已在管
6 URL / 主题 config 键 _config.yml 人/JSM ✅ 消费中 可选
7 Chrome 文案 ui-strings(未来) _data/ui_strings.yml JSM ⬜ 未建 ⬜ 未来

图例:✅ 完成 · 🟡 部分 · ⬜ 未做


契约 1 —— 首页板块 _data/home_blocks.yml

JSM-owned artifact。 Jekyll 首页完全按此文件渲染;JSM 写它、并把每个子站的 Latest Posts JSON 注册为一个 source加子站 = 往这里加一个 block,Jekyll 模板一行不改。

消费方:_layouts/home.html_includes/blocks/render.html_includes/blocks/{featured-swiper,subsite-feed,post-list}.html

顶层结构

schema: home-blocks.v1          # 版本头,JSM 校验/升级用
blocks:                         # 数组;同一 slot 内按数组顺序渲染
  - { type: , slot: ,  }

block 字段

| 字段 | 必填 | 取值 | 说明 | |—|—|—|—| | type | 是 | featured-swiper / subsite-feed / post-list / series / archives / taxonomy-tags / taxonomy-cats | 未知 type 渲染器静默跳过(向前兼容) | | slot | 是 | nav / hero-left / hero-right / main-top / main / sidebar / footer | 决定渲染到哪个区 | | source | 视 type | own(自有 Jekyll 内容)/ 绝对 URL / 站内相对路径 | subsite-feed 用 URL;post-list/featured-swiper 用 own | | variant | subsite-feed 必填 | focus-list / cards / compact / ticker | 未知 variant 按 compact 兜底cards 渲染为可滑动 Swiper(桌面一屏 3 张 / 移动 2 张,两侧箭头翻页)——count 建议 3 的倍数(如 9) | | count | 否 | 整数 | 显示条数;缺省 6。“子站少→count 大填满竖条;子站多→多 block 各 count 小” | | title | 否 | 字符串 | 板块小标题(kicker) | | more_url | 否(subsite-feed) | URL | 板块底部渲染「更多 →」,链到子站页面(feed 只显前几条时用;静态、独立于 feed 加载,feed 挂了链接仍在)。JSM 注册子站时一并提供其页面 URL | | autoplay | featured-swiper | 布尔 | 默认 false(手动 swiper) | | fill_count | 否(featured-swiper) | 整数 | swiper 下方渲染 N 条 featured: true 文章的文字链列表,把 hero-left 垫到与 hero-right 等高;缺省 0(不渲染) | | paginate | post-list | 整数 | 每页条数;缺省 6 | | id | 否(建议 JSM 加) | 稳定字符串 | JSM 编辑器做增删排序 diff 的稳定 key(渲染器目前不依赖,但建议带上) | | count(series) | 否 | 整数 | 专题系列板块(type: series,2026-07-06;缺省 title「专题系列」、count 5):列出各系列(按最近文章先后,取前 count 个),每个链到 /series/<名称>/;系列数 > count 时显示「更多 →」链到 /series/ 总览页。系列来自文章 front matter series: 名称(+ 可选 series_index: N,缺省按日期先后计序);文章页标题下渲染「名称(一)」标记并链到系列页。系列为空时整块不渲染。per-系列页需 JSM 生成 _series/<名称>.mdlayout: seriespageseries: 名称,可选 title/description)——同 tag/category 机制;/series/ 总览页由 Jekyll 从文章聚合,无需生成 | | —(archives) | — | — | 往期回顾板块(2026-07-06,type: archives,可选 title 缺省「往期回顾」):各年度 + 本年度各月份做成横向 chip 流(满行换行),不显篇数。链接指向 JSM 生成的 _yearly/YYYY/)/_monthly/YYYY-MM/)页面;对应页面不存在时渲染为纯文本(JSM 补页后自动变链接) |

槽位与布局

  • nav → 导航区(目前 taxonomy-cats 用;由 header.html 直接渲染分类下拉,暂不从 home_blocks 读取——见备注)
  • hero-left / hero-right → 首屏两栏(左轮播 / 右焦点 feed)
  • main-top → 主栏顶部(社媒卡片)
  • main → 主栏(最新文章列表)
  • sidebar右侧竖条,专用于子站 feed 板块栈(从上往下叠,adaptive count)
  • footer → 预留

当前默认(v4,Jekyll 侧手写,待 JSM 接管)

schema: home-blocks.v1
blocks:
  - { type: featured-swiper, slot: hero-left,  source: own,   autoplay: false, title: 精选, fill_count: 2 }
  - { type: subsite-feed,    slot: hero-right, source: "https://de-ccp.org/daily/assets/data/picked/top-news.json", variant: focus-list, count: 9, title: 焦点头条 }
  - { type: subsite-feed,    slot: main-top,   source: "https://de-ccp.org/i-pulse/assets/data/latest_posts.json", variant: cards, count: 9, title: 社媒快讯, more_url: "https://de-ccp.org/i-pulse/" }
  - { type: post-list,       slot: main,       source: own,   variant: list,    paginate: 6, title: 最新文章 }
  - { type: subsite-feed,    slot: sidebar,    source: "https://de-ccp.org/i-news/assets/data/latest_posts.json", variant: compact, count: 8, title: 新闻速览, more_url: "https://de-ccp.org/i-news/" }
  - { type: subsite-feed,    slot: sidebar,    source: "https://de-ccp.org/rights-watch/assets/data/latest_posts.json", variant: compact, count: 8, title: 维权动态, more_url: "https://de-ccp.org/rights-watch/" }

JSM 要做

  • 提供编辑/写入 _data/home_blocks.yml 的能力(读子站注册表 → 生成 block;保持 schema 头)。
  • 文件头保留 # JSM-owned artifact 注释;写入用原子替换。
  • (建议)给每个 block 生成稳定 id

备注:taxonomy-cats(分类导航下拉)目前由 header.html 直接用 site.category 渲染,未从 home_blocks 读取。若将来要让 JSM 控制导航分类,可扩展渲染器读 slot: nav


契约 2 —— 子站 Latest Posts JSON(subsite-feed 数据源)

通用组件 assets/js/subsite-feed.js 在浏览器端 fetch 每个 subsite-feed block 的 source URL 并渲染。这正是 JSM latest_posts.py 的产物,字段已对齐。

item 字段(每条)

[
  {
    "title":     "标题",              // 必填
    "link":      "https://…",         // 必填,目标链接
    "pubDate":   "2026-07-03",        // 可选,compact/cards 显示时间
    "cover_img": "https://…",         // 可选,cards 变体的图;缺失则 cards 降级为纯文字卡
    "weight":    20260703000000        // 可选,排序权重(大在前)
  }
]

(已核对真实 de-ccp.org/i-news/.../latest_posts.json,字段完全一致。)

组件行为

  • 排序weight 降序;并列或缺失时按 pubDate 降序。
  • 变体用字段focus-list = title/link(首条大标题 + 其余列表);cards = cover_img/title/pubDate;compact = title/link/pubDate;ticker = title/link。
  • 降级三态:加载中「载入中…」/ 空数组或空暂无内容/ fetch 失败加载失败空 feed 不报错、优雅显示空态。
  • 安全:所有文本 escapeHtmllink/cover_img 只放行 http(s):// 与站内 /…(掐 javascript:)。
  • CORS:跨域 feed(de-ccp.org)需返回 Access-Control-Allow-Origin: *(GitHub Pages 默认满足)。

当前接线状态

| 板块 | source | 状态 | |—|—|—| | 焦点头条 | de-ccp.org/daily/assets/data/picked/top-news.json | ✅ 真实(旧 picked 格式,仅 title/link/weight,组件已兼容降级) | | 新闻速览 | de-ccp.org/i-news/assets/data/latest_posts.json | ✅ 真实 | | 社媒快讯 | de-ccp.org/i-pulse/assets/data/latest_posts.json | ✅ 真实(含 cover_img,cards 变体带图) |

feed 语言:当前三个子站 feed 全是中文内容——包括新闻速览/Formal News(它是从英语新闻转述翻译而来的中文新闻,不是英文)。所以没有”天生英文、免翻译”的子站。若要首页 feed 也能切英文,JSM 需为每个子站逐条翻译标题、产出并列的 latest_posts.en.jsonsubsite-feed.js 在英文模式抓 .en 变体(home_blocks.ymlsource_en 或约定 .en 后缀)。属首页 i18n(E slice 2+),当前未做。

JSM 要做

  • 社媒快讯真实 feed(i-pulse)已上线并接线;三个首页 feed(daily / i-pulse / i-news)均为真实源。
  • 长远把旧 picked top-news.json(仅 title/link/weight)统一成标准 latest_posts + overrides 形态,全站只剩一种 feed 形状。
  • (可选)latest_posts 输出加 category 字段,则 cards 可显示分类。

契约 3 —— 逐篇翻译 i18n(JSM 侧最大一块

方案 A:中文永远是页面的真实 HTML(利中文 SEO),英文按需由 JS 从翻译 JSON 加载、作为副本插到中文块后,切换 中/英/双语。前端已全部就绪(assets/js/article-i18n.js + assets/css/article-i18n.css + _includes/article-lang-switcher.html,post/news 布局按 page.translations 门控挂载)。JSM 只需产出三样东西:front matter、翻译 JSON、正文 block id。

① 文章 front matter(JSM 写入 CN 文章)

lang: zh
default_lang: zh
translations:
  en: /assets/i18n/<c_id>.en.json     # 同源路径;可多语言 translations: { en: …, ja: … }

translations 时,布局才加载 i18n 资源、插入切换器、给 <h1>data-i18n-title

② 正文 block id(JSM 注入到 CN markdown)

每个可翻译的正文块要有稳定 data-i18n="<id>"。Kramdown 用 IAL 语法:

这是中文第一段。
{: data-i18n="b_a1c3" }

## 小标题
{: data-i18n="b_7f20" }

> 引用。
{: data-i18n="b_9e0d" }

关键约束:id 必须稳定、内容锚定,绝不能用位置序号(p1/p2/p3)。 否则作者中间插一段就全体错位、翻译全对不上(和 tag 手工维护烂掉是同一种病)。建议 JSM 像发 c_id 那样发短 uuid 或内容 hash,并在编辑时维护映射。

block 模型(建议 JSM 采纳的可翻译元素集):段落 <p>、标题 h2–h4、引用 blockquote、图说 figcaption、列表项(可选)。图片/表格本体共享不译(其 alt/caption 可另做)。

③ 翻译 JSON(JSM 生成,放同源 assets/i18n/

{
  "post_id": "<c_id>",
  "lang": "en",
  "title": "English Title",           // 对应 data-i18n-title 的 <h1>
  "blocks": {
    "b_a1c3": "First English paragraph.",           // 纯文本 → textContent(安全)
    "b_7f20": "An English Heading",
    "b_9e0d": { "type": "html", "content": "English with <a href=\"/x/\">link</a>." }  // 需要链接/加粗时 → innerHTML
  }
}
  • blocks 的 key 必须与正文 data-i18n 一一对应
  • 纯文本值 → textContent(最安全);富文本用 { "type": "html", "content": … }innerHTML(JSM 自产、同源、可信)。

④ 对齐校验(JSM check-script —— 命门)

JSM 发布时必须校验:正文里的每个 data-i18n 都有对应的 blocks key,反之亦然。否则文章一多,必然出现”中文有这段、英文漏翻”或”JSON 有翻译、正文没对应块”。这条是整个方案能不能长期不烂的关键。

前端已实现(JSM 无需管)

  • ?lang=zh|en|both 可分享链接 + localStorage 记忆;<html data-article-lang> + CSS 控三态;标题切换;按钮 aria-pressed
  • 中文原文始终在 DOM,切回中文零成本、不丢原文。
  • ?lang=en 变体的 SEO canonical 已自动指向干净中文 URL(jekyll-seo-tag),不稀释中文收录。

兼容:未翻译 / 部分翻译(前端已保证,JSM 无需特殊处理)

  • 未翻译的文章:只要不写 translations front matter → 不出现切换按钮、不加载任何 i18n CSS/JS,就是普通中文页、零开销。所以”有的翻译、有的不翻译”是默认兼容的混合状态;JSM 只给真正翻了的文章写 translations.en 即可。
  • 部分翻译:某篇有 translations.en 但 JSON 只翻了一部分块 → 切英文时有译文的块显示英文、没译文的块自动保留中文(JS 只给有译文的块加隐藏类)。整篇 / 部分 / 不翻译三种都不会出错。

JSM 要做

  • CN 文章 front matter 写 translations.en(+ default_lang)。
  • 注入稳定 data-i18n block id(非位置式)+ 维护映射。
  • 生成 assets/i18n/<c_id>.en.json(title + blocks,key 对齐正文)。
  • check-script 校验正文 data-i18n ↔ JSON blocks 对齐。
  • [ ](可给 JSM 编辑器加:翻译面板、”标记段落可翻译”命令、?lang= 预览)。

契约 4 —— Taxonomy 词条文件(冻结,勿改结构

JSM 生成/维护,Jekyll 消费。

  • 位置:_tag/<name>.md_category/<name>.md(对应 Jekyll tag/category collection)。
  • front matter 形状:
    ---
    layout: tagpage          # 或 categorypage
    tag: "六四"              # 或 category:
    title: "标签: 六四"      # 或 "分类: X"
    ---
    
  • 词条描述(可选):_metas/tags.md 前置里 name: tags + <词条>: <描述> 映射(tag-info.html 消费)。
  • Jekyll 侧:标签/分类页、导航分类下拉、首页热门标签、/tags/ 全部标签页都从这里来。加词条忘了建文件 → 页面缺失,所以由 JSM 保证词条文件齐全(含跨子站文章的引用)。

契约 5 —— 文章版本身份(冻结

JSM 拥有版本模型,Jekyll 按它去重取最新版。

  • 字段:c_idversion_ofbase_c_idupdate_date
  • update_date 必须是完整时间戳 %Y-%m-%d %H:%M:%S(Jekyll 去重 Liquid 按此格式比较;仅日期会在同日多版本时并列出错)。
  • Jekyll 侧去重已收敛到单一 _includes/latest-versions.html(home/all-tags/all-categories/tagpage/categorypage 共用)。post/news 页另用 O(n) 单点查找取”本文最新版”。
  • H9 已实现、默认未启用(2026-07-06):JSM 发布新版本时自动给旧版打 superseded: true、删除最新版时撤标,并提供存量回填/对账 API(superseded-status / superseded-backfill)。Jekyll 侧 _includes/latest-versions.html 已双轨:_config.ymlversion_dedup: superseded 切 O(n) where_exp 快路径,缺省 liquid 仍走 O(n²) 自纠错老路。启用顺序必须先回填后切 flag,步骤见 JSM docs/SUPERSEDED_VERSIONS.md

契约 6 —— config 键(_config.yml

  • url / test_url:canonical 生产域 / JSM 测试链接域(JSM v0.31.19 public_url_resolver)。Jekyll 的 SEO/sitemap 用 url
  • ui_theme:主题选择键(ui_theme 而非 Jekyll 保留键 theme)。缺省 neutral;JSM 可写它来切换默认主题(可选值 neutral / ink-wash)。
  • lang: zh_CN:站点语言(jekyll-seo-tag 的 og:locale)。
  • 搜索范围开关 global_search(布尔)+ search_peers(绝对 URL 列表):本站索引 assets/data/search.json(构建时生成:latest-versions 去重 + 滤 hidden,字段 title/url/date/tags恒被搜global_search: true 时额外合并 search_peers 里各站点的 search.json(跨站全局搜索),false 只搜本站。search_peers绝对地址(各站点域/子路径不同)。生产同域(de-ccp.org)时同源;跨域抓取需目标资源域放行 CORS,否则该 peer 被 search.js 静默跳过(不影响本站结果)。子站 search.json 的 url 必须是带 repo 子路径的绝对地址 {url}/{repo}{文章路径}(如 https://de-ccp.org/i-news/2026/...,与 latest_posts.json 的 link 同规),样板用 site.url+site.repo 生成。JSM 维护:每加一个子站,往主站(及需要全局搜索的子站) search_peers 追加一行其 search.json 绝对地址。
  • url: https://de-ccp.org(2026-07-05 起):主站与全部子站共用同一 url。子站标准形态(经 GitHub 实测,2026-07-05 修正)baseurl: ""(必须为空)+ repo: "<仓库名>"(子路径由该自定义键表达,子站公开地址 = {url}/{repo}/)+ src_url(子站引用主站 css/js 的域;测试期指 https://test.qugonghua.org,上线改为 https://de-ccp.org)。旧说法「子站 baseurl: /<仓库名>」作废。子站样板:同级目录 ../subsite-template/(独立 git 仓库,README 有三步上手),新子站从它复制。

契约 7 —— Chrome 文案 ui-strings(未来,E slice 2 才需要

站点级 EN⇄中 切换(导航按钮翻转整站 chrome 文案)需要一套英文 ui-strings。尚未实现(Jekyll 侧未建、JSM 侧未做)。若将来做,建议:

  • _data/ui_strings.yml{ zh: {...}, en: {...} },key 覆盖导航(分类/联系/关于)、板块标题(头条/焦点头条/社媒快讯/最新文章/新闻速览/热门标签/全部标签)、分页(上一页/下一页/第 X 页)、文章标签(分类/标签/作者)、页脚等。
  • Jekyll 侧给 chrome 元素加 data-i18n-ui="<key>",一段小 JS 按站点语言偏好替换。
  • 需要先定英文措辞(内容决策),宜由 JSM 统一管理(和它管 home_blocks / 翻译 JSON 一致)。
  • home_blocks.ymltitle 是数据,届时也需要 en 对照(title_en 或 ui-strings 映射)。

JSM 行动清单(按优先级)

P0(让首页数据全通)

  1. 提供社媒快讯真实 feed URL(契约 2)。
  2. 接管写 _data/home_blocks.yml(契约 1)。

P1(上线中英翻译)

  1. i18n 三件套:front matter + 稳定 data-i18n id + 翻译 JSON + 对齐 check(契约 3)。

P2(长期健壮)

  1. 统一子站 feed 形状(弃旧 picked 格式);latest_posts 可选加 category
  2. 版本 superseded 标记,解 O(n²)(契约 5 backlog)。
  3. 若要站点级 EN 切换:ui-strings(契约 7)。

冻结(勿破坏):契约 4(taxonomy 文件形状)、契约 5(c_id/version_of/update_date 格式)、契约 6(url/test_url/ui_theme)。

</div>

</div> </main> `。

  • A2 head 收敛:删掉 home.html 整份复制的 <head>,home 改 layout: default
  • A3 post/news/page 只留内容区(去掉各自 <body>);tag/category/archive 经 page 继承,无需改。
  • A4 <html lang="zh-CN">(在 default)。
  • 验证:jekyll build 通过;home/post/news/about/56 tag/10 category 均单一 html/head/body/title。commit f381d83

B. 配置驱动主页板块(核心)

  • B1 _data/home_blocks.yml(v4 默认组合 + schema: home-blocks.v1)。commit 67035b6
  • B2 home.htmlhome_blocks、按 slot 分区、经 blocks/render.htmltype 分派。
  • [~] B3 板块 include(_includes/blocks/):
    • featured-swiper.html —— 自有 置顶+featured,手动 swiper(honour block.autoplay)。
    • subsite-feed.html + assets/js/subsite-feed.js —— 参数化 data-feed-url/-variant/-count;变体 focus-list/cards/compact/ticker
    • post-list.html —— 自有 posts,分页,复用 latest-versions.html
    • taxonomy-tags:首页底部热门 15(all-tags.htmllimit/title + 空条目守卫)+ 「全部标签 →」→ 新 /tags/ 页(列全部已用标签)。不进右侧栏(右侧栏留给子站板块)。
    • taxonomy-cats → 导航 分类 ▾ 下拉(header.htmlsite.category 10 项)+ 撤底部分类带。桌面 hover/focus 展开、移动端点击展开(header.js)。commit a6b8343 + 触屏切换。
  • B4 通用化 top-news.jssubsite-feed.js:读 data-feed-url(del 硬编码 de-ccp)。
  • B5 subsite-feed JSON 契约:title/link/pubDate/cover_img/weightcover_img 缺失降级;空/失败三态;空 [] feed 优雅降级。
  • B6 已核对 JSM latest_posts.py(2026-07-03):item = {title, link, pubDate, cover_img, weight}。有图(cover_img)、有日期(pubDate);category/excerpt → 卡片先不放分类。JSM 有手动 overrides 层(置顶/改标题图)。

C. 版本去重收敛 + 死代码

  • C1 版本去重收敛到共享 _includes/latest-versions.htmlinclude.collection 参数):home + all-tags + all-categories + tagpage + categorypage 全部复用;4 份内联 O(n²) 拷贝删除、计数逐一比对不变(中共 10 / 政治 33 / 52 标签 均一致)。post/news 的”取本文最新版”是另一种单点查找(O(n)),保留。终局仍是 JSM 产出已解析列表(H9)。
  • C2 删除死过滤器 wrap_tables / add_post_img_class(post/news/page);表格包装/图片加类仍由 post/news 的客户端 JS 负责。commit 34b918e
  • C3 分页收敛:单一 page-size 来源;清除 index.html 里已弃用的 jekyll-paginate-v2 残留配置。

D. Correctness bug(Fable 已在产物中确认)

  • D1 /about//contact/ 各有根目录版 + _pages 版争同一 URL —— 各留一份。
  • D2 每页两个 <title> —— 已去掉布局手写的,只留 `
Opus 架构阶段 · 落地清单 | 去共化

(随 A 段完成,commit f381d83`)。

  • [~] D3 域名归一:robots.txt Sitemap 改走 absolute_url(= site.url),已修(commit 34b918e)。top-news.js 硬编码 de-ccp.org 留 B4 一并处理。
  • D4 中文 SEO(首要)lang: zh_CNog:locale 现为 zh_CN)+ 中文 description;单一 <title> 见 D2。commit 34b918e
  • D5 footer site.version vs config 里的 ver —— 键名对齐。
  • D6 无效 HTML:footer 多余 </p></br><br>、post/news 未闭合 <span class="post-link-date"> 全部修复并验证(版本文章 span 6 开 6 闭平衡)。
  • D7 调试注释全清(all-tags/all-categories/tagpage/categorypage/post/news);_metas/tags.md 误粘的 404 HTML + 测试项已删,保留 tag_info 数据。
  • D8 @font-face format truetypewoff2(commit d610298);swiper/top-news 已移到 body 末尾 defer(随 A 段)。

E. EN i18n(方案 A 先行 / B 后备)

  • E1 文章页:page.translations 存在时 default.html 加载 article-i18n.css + article-i18n.js(defer);post/news 插 article-lang-switcher.html(中/English/双语)。commit 4a9d4db
  • E2 article-i18n.js:按需 fetch page.translations.en(同源),英文作副本插到中文块后(不覆盖);html[data-article-lang=zh|en|both] + CSS 控三态;?lang= + localStorage + 标题切换 + aria-pressed。
  • [~] E3 标题 data-i18n-title 已挂;正文块 data-i18n 前端已支持(临时夹具用 Kramdown IAL 验证通过)。稳定 id 由 JSM 分配(非位置式)——JSM 侧待做。
  • [~] E4 /en 404 死链已去(nav + 汉堡菜单)。剩 slice 2:站点级 EN⇄中 切换按钮 + chrome 文案(_data ui-strings,需定英文措辞、宜与 JSM 协调)+ 驱动文章默认语言。
  • E5 安全:纯文本 block 用 textContenttype:"html" block innerHTML(JSM 生成、同源、可信)。
  • E6 JSM 侧(后做):翻译 JSON 面板、自动生成 <c_id>.en.json、注入/维护稳定 block id、check-script 校验正文 data-i18n ↔ JSON blocks 对齐。
  • E7 B = backlog:独立 EN 页(SEO)+ hreflang,用同一 JSON 生成。
  • E8 中文 SEO 护栏:jekyll-seo-tag 已输出干净 rel=canonicalpage.url,不含 ?lang)→ ?lang=en 变体自动归一到中文 URL,无需额外处理。已核验产物。

F. 主题化 + 交给 Fable 的皮肤接口(可切换主题,不写死单一主题)

  • F1 token 契约 _sass/_tokens.scss:per-theme mixin 应用到 :root + [data-theme](全套 –font/–paper/–surface/–band/–ink/–accent/–line/–radius)。commit d610298
  • F2 主题层:themes/_neutral.scss(默认)+ themes/_ink-wash.scss,各一 @mixin:root{@include theme-neutral} + [data-theme="neutral"|"ink-wash"]。加主题 = 一文件 + 一行。
  • F3 桥接→直连:_styles.scss/_blocks.scss 硬编码色已全量var(--token),旧 $var 桥接层删除(Fable 2026-07-04)。
  • F4 <html data-theme="neutral">ui_theme 非保留键 theme);两主题空壳都建;config ui_theme: 可选(缺省 neutral)。运行时切换/深色模式机制预留。commit d610298
  • F5 class 名稳定(JS/swiper/i18n 钩子未改名);Fable 皮肤已交付:token 正式值 + 组件精修 + 宋体标题(Noto Serif/Sans SC webfont 接入)+ 朱红 .hero-badge(唯一 –accent-2 用点)。运行时切 data-theme="ink-wash" 验证过 token 全翻转。
  • F6 neutral/ink-wash 两主题成套值已定稿(Fable 2026-07-04;两主题 –ink-3 均加深至 AA)。

G. 验证

  • G1 bundle exec jekyll build 通过。
  • G2 对比重写前后生成的 URL 清单,permalink 不变(外链不断)。
  • G3 冻结的 JSM 契约(UPGRADE_PLAN §5)未被破坏。

H. Fable 审查补充(2026-07-03 并入)

  • H1 【必须】主题键用 ui_theme:不用 Jekyll 保留键 theme:(撞 gem 主题查找会炸构建)。
  • H2 【必须】Sass 3.7.4(jekyll-sass-converter 1.5.2):token/主题层只用 @import,禁 @use/@forward
  • [~] H3 feed 三态 + loading=lazy + cards object-fit:cover/3:2 + 按变体预留 min-height 防 CLS(commit)已做。剩余:确认跨域(de-ccp)feed 的 CORS 头。
  • H4 home_blocks.ymlschema: home-blocks.v1;渲染器未知 type/variant 静默跳过;同 slot 顺序=数组顺序;文件头注明 JSM-owned。(块 id、空 slot 撑满布局待 slice 2。)
  • H5 feed 安全:escapeHtml + safeUrl 协议校验(仅 http/https、相对路径)。commit 67035b6
  • H6 分页页(/page/2+):home_blocks 每块加 first_page_only,第 2 页起只留列表。
  • H7 图片纪律:首页缩略图(post-list + feed cards)统一 aspect-ratio: 3/2 + object-fit:cover,无图时留 –band 占位框(Fable 2026-07-04)。
  • H8 可达性:文字对比 ≥4.5:1(两主题 –ink-3 实测加深:neutral #767066=4.7:1、ink-wash #6f6555=5.0:1);swiper keyboard 模块启用(Swiper 11 a11y/aria 默认开);a:focus-visible 全局 outline(Fable 2026-07-04)。
  • H9 版本去重终局(JSM backlog):publish 时给旧版本打 superseded: true → Liquid 退化为 O(n) where_exp;C1 的 include 认 superseded 标记走快路径。
  • H10 杂项:ver/port 等 config 清理;posts(无尾斜杠)/news(有尾斜杠) permalink 保持原样(勿”统一”以免断链)。