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/<名称>.md(layout: seriespage,series: 名称,可选 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 不报错、优雅显示空态。 - 安全:所有文本
escapeHtml;link/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.json,subsite-feed.js在英文模式抓.en变体(home_blocks.yml加source_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 无需特殊处理)
- 未翻译的文章:只要不写
translationsfront matter → 不出现切换按钮、不加载任何 i18n CSS/JS,就是普通中文页、零开销。所以”有的翻译、有的不翻译”是默认兼容的混合状态;JSM 只给真正翻了的文章写translations.en即可。 - 部分翻译:某篇有
translations.en但 JSON 只翻了一部分块 → 切英文时有译文的块显示英文、没译文的块自动保留中文(JS 只给有译文的块加隐藏类)。整篇 / 部分 / 不翻译三种都不会出错。
JSM 要做
- CN 文章 front matter 写
translations.en(+default_lang)。 - 注入稳定
data-i18nblock id(非位置式)+ 维护映射。 - 生成
assets/i18n/<c_id>.en.json(title + blocks,key 对齐正文)。 - check-script 校验正文
data-i18n↔ JSONblocks对齐。 - [ ](可给 JSM 编辑器加:翻译面板、”标记段落可翻译”命令、
?lang=预览)。
契约 4 —— Taxonomy 词条文件(冻结,勿改结构)
JSM 生成/维护,Jekyll 消费。
- 位置:
_tag/<name>.md、_category/<name>.md(对应 Jekylltag/categorycollection)。 - front matter 形状:
--- layout: tagpage # 或 categorypage tag: "六四" # 或 category: title: "标签: 六四" # 或 "分类: X" --- - 词条描述(可选):
_metas/tags.md前置里name: tags+<词条>: <描述>映射(tag-info.html 消费)。 - Jekyll 侧:标签/分类页、导航分类下拉、首页热门标签、
/tags/全部标签页都从这里来。加词条忘了建文件 → 页面缺失,所以由 JSM 保证词条文件齐全(含跨子站文章的引用)。
契约 5 —— 文章版本身份(冻结)
JSM 拥有版本模型,Jekyll 按它去重取最新版。
- 字段:
c_id、version_of、base_c_id、update_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.yml的version_dedup: superseded切 O(n)where_exp快路径,缺省liquid仍走 O(n²) 自纠错老路。启用顺序必须先回填后切 flag,步骤见 JSMdocs/SUPERSEDED_VERSIONS.md。
契约 6 —— config 键(_config.yml)
url/test_url:canonical 生产域 / JSM 测试链接域(JSM v0.31.19public_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.yml的title是数据,届时也需要 en 对照(title_en或 ui-strings 映射)。
JSM 行动清单(按优先级)
P0(让首页数据全通)
- 提供社媒快讯真实 feed URL(契约 2)。
- 接管写
_data/home_blocks.yml(契约 1)。
P1(上线中英翻译)
- i18n 三件套:front matter + 稳定
data-i18nid + 翻译 JSON + 对齐 check(契约 3)。
P2(长期健壮)
- 统一子站 feed 形状(弃旧 picked 格式);
latest_posts可选加category。 - 版本
superseded标记,解 O(n²)(契约 5 backlog)。 - 若要站点级 EN 切换:ui-strings(契约 7)。
冻结(勿破坏):契约 4(taxonomy 文件形状)、契约 5(c_id/version_of/update_date 格式)、契约 6(url/test_url/ui_theme)。