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)。