1 - OINK 实施日记:从复制外壳到统一主题
OINK 始于一个令人不安的事实:多个生产文档站之所以看起来相互关联,是因为它们确实源于同一套实现;但公共实现却以复制文件的形式散落在各处。呈现效果足够一致,维护模型却并非如此。
这篇日记记录项目如何从重复站点覆盖项走向一款直接演化的统一主题。它关注决策与证据,而不是逐条复述提交历史。
锁定产品契约
第一项真正有价值的工作是做减法。选择实现方式之前,我们先写清产品必须是什么:
- 从 Docsy 直接演化而来的独立主题;
- 唯一标准外壳,而不是可切换皮肤;
- Hugo Extended 是消费端唯一构建依赖;
- 所有主题自带浏览器资源默认本地优先;
- 多语言行为从 Hugo 推导,而不是从 PGSTY 域名推导;
- 可复用组件进入主题,业务语义留在站点;
- 保留 Docsy 历史、许可证与可追踪的上游关系。
这排除了一个看似诱人、实际代价高昂的捷径:增加 params.oink.enabled
并保留旧外壳。模式开关会让每次布局调整、无障碍修复与测试都支持两套产品。直接演化则让目标设计成为唯一设计。
替换页面外壳
文档、博客与 API 参考布局围绕一组小型共享 partial 重新构建。新的外壳包括:
- 全局导航与响应式次级导航;
- 可调整宽度、可折叠的侧栏;
- 本地搜索与快捷链接;
- 语言与颜色模式控件;
- 面包屑、目录(TOC)、页面元数据与反馈;
- 一致的页脚与打印布局。
真正困难的不是画出导航栏,而是在删除复制的 baseof.html
时保留 Docsy 既有扩展点。范围明确的 hook 仍然存在;复制整个站点外壳不再是正常的定制路径。
移除消费端工具链
原有依赖链假设 Bootstrap 与 Font Awesome 来自 npm,部分路径还会调用 PostCSS。OINK 把必需源码与编译产物移入主题,并让 SCSS 留在 Hugo 自身的 Asset Pipeline 中。
测试不只检查 hugo
是否成功。fixture 中的陷阱会在消费端构建尝试运行 Node.js、npm、PostCSS 或 Autoprefixer,或模板调用
resources.GetRemote 时立即失败。LTR 与 RTL 页面遵守同一约束。
这里的区分非常重要:仓库仍使用 Node 运行维护者测试工具。“仅依赖 Hugo”描述的是消费站点取得完整主题后所需的构建环境,并不是禁止主题仓库使用开发工具。
纳管浏览器运行时
下一层工作覆盖浏览器原本可能从远端获取的全部依赖:Bootstrap、Font Awesome、字体、jQuery、Lunr、Mermaid、KaTeX、Markmap、Swagger UI、Redoc、Asciinema、ECharts、Infographic 及其辅助库。
theme/VENDOR.json
为每项选定产物记录来源、固定版本、许可证路径、校验值与更新流程。许可证与 vendor 内容相邻存放。清单会与真实文件一起验证,而不是停留在愿望列表。
PlantUML 与 Diagrams.net 迫使我们做出一项重要区分:它们依赖服务,而不只是 JavaScript 库。OINK 拒绝虚构公共端点;启用功能却没有配置服务时,构建会失败。
构建多语言内核
旧的语言行为散落在导航逻辑与站点专用假设中。新的内核从 Hugo 已配置语言、.Translations
与 .AllTranslations 出发。
呈现方式刻意保持稳定:只有一种语言时隐藏选择器;两种或更多语言统一使用同一个图标按钮。点击会按配置权重切换,短暂悬停或键盘聚焦则打开完整语言菜单。
缺少译文时回退到目标语言首页。语言名称使用该语言的自称。同一组对象还会驱动
lang、书写方向、canonical、 hreflang 与 Open Graph
locale 元数据,因此可见选择器不会与 SEO 输出漂移。
测试会让 RTL 语言作为当前语言覆盖每种状态,而不只验证 starter 的两种 LTR 语言。原生链接与 disclosure 控件让键盘行为保持可预期。
提炼通用组件
Asciinema、ECharts、Infographic、文档轮播、折叠块、标签页、卡片与参数渲染,已经在 PGSTY 站点证明了价值。接下来的工作,是把复制品转化为产品 API:
- 统一参数名称与默认值;
- 根据页面身份与短代码序号生成唯一 ID;
- 每页只加载一次运行时,未使用页面完全省略;
- 保持子路径 URL 正确;
- 支持多个内容完全相同的实例;
- 提供打印、深色模式、移动端、键盘与减少动态效果行为;
- 为导入内容保留兼容别名。
产品矩阵等业务控件没有迁入主题。是否复用不能只看有多少仓库包含同一个副本;通用组件必须拥有稳定、与业务无关的契约。
支持 ECharts 回调
ECharts 回调是 JSON 与 YAML 无法表达的合法图表选项。现有页面用它们格式化提示、标签以及按数据选择颜色。把这些回调单独视为迁移例外,只会增加配置,并不会形成沙箱。
因此短代码采用一套直接契约:
- 内容提供 JSON 或 YAML,由 Hugo 解析并安全序列化;
- 可选的 JavaScript 围栏代码块声明回调;
$fn:name在选项解析后重新连接这些回调;- 作者按照行内 HTML 与其他自定义集成相同的信任模型审查可执行代码。
测试覆盖内容相同的重复图表、非法 CSS 长度、结构化选项与回调注册。
创建 starter 与归档
如果最小示例能完整演示契约,契约就更容易获得信任。starter 包含双语首页、文档、博客与组件页面,以及本地搜索、深色模式、图表、API 文档和新增组件;它没有
package.json,也没有站点工作流。
离线打包器会组合
theme/、starter/、许可证、上游记录与迁移指南,并排除生成结果与依赖缓存。它会写出配套 SHA-256 文件,并拒绝覆盖已有产物。
验收测试把 starter 与主题复制到临时目录,清空缓存,阻断 HTTP/HTTPS 与 Go 代理,使用 Hugo 构建,再检查 HTML 与 CSS 中是否出现第三方子资源。
演练四站迁移
SILO、PGSTY、SOW 与 Pigsty 提供了现实检验。演练工具会复制各工作区而不是修改源目录,只删除已经分类的公共覆盖项,应用本地主题 replacement,禁止网络与前端工具,再运行生产构建。
最近一次演练分别从 SILO、PGSTY、SOW 删除 20 个公共覆盖项,从 Pigsty 删除 24 个。Pigsty 保留三个业务矩阵短代码与现有 ECharts 回调。所有临时副本均成功构建,分别生成 1,095、16、128 与 2,473 个 HTML 文件。
这些数字证明的是记录提交上的迁移演练,不代表任何生产仓库已经改变,也不代表任何托管站点已经部署。
把样例站变成 OINK 文档
继承而来的 docsy.dev
站点是很有价值的回归语料库,但它只描述 Docsy。文档阶段完成了四项工作:
- 把英文设为首要语言、简体中文设为第二语言;后续外壳评审又从演示站点移除了法文;
- 为每份核心文档与博客源文件创建并置的
.zh.md译文; - 在每个中文标题中显式保留英文标题 ID;
- 增加 OINK 产品指南、项目公告与本实施日记。
翻译之前,我们先建立术语与排版指南。随后使用检查器验证源文件与译文配对、标题数量、中文显式 ID,以及渲染后的中英文标题 ID 是否完全相同。
Docsy 历史发布文章保持忠实翻译,其中的 npm 时代说明属于历史语境;OINK 架构与迁移指南则明确说明当前仅依赖 Hugo 的产品契约。
测试如何改变设计
多项测试不仅验证实现,也反过来改变了设计:
- 子路径 fixture 迫使每个本地组件 URL 都经过 Hugo URL 处理;
- 重复实例测试用“页面加序号”ID 替代内容哈希;
- 离线浏览器检查暴露了隐含运行时请求;
- RTL 语言矩阵避免选择器只适用于 starter 的两种 LTR 语言;
- ECharts 回调 fixture 保证回调注册与结构化选项可以协同工作;
- 迁移演练保留了直接清空
layouts/时会被误删的站点专用 partial。
最有力的测试套件约束的是产品边界,而不只是当前 HTML 快照。
后续工作
目前仍有两个发布关卡有意保持开放。公开品牌、仓库、模块与软件包身份,以及首个版本需要批准。随后还要让真实 Cloudflare Pages 项目从源分支构建,并通过托管验证。
生产迁移应逐站进行,使用专用分支、预览部署、视觉回归与回滚产物。四站临时演练是这项工作的基础,不能替代正式迁移。
经验总结
- 移动文件前先写清产品边界。
- 本地优先承诺必须同时具有构建阶段与浏览器阶段证据。
- 配置应表达用户选择,而不是内部实现分支。
- 翻译质量不仅是正文,还包括稳定链接、代码保真、排版与渲染结构。
- 复用应消除维护副本,而不能吞并业务语义。
- “构建”“打包”“公开发布”“部署”与“迁移”是不同声明,需要不同证据。
最终成果没有重写那样戏剧化,却更加实用:一款能够作为完整产品被理解、构建、测试、翻译与迁移的统一主题。
2 - OINK 实现预览正式亮相
今天,我们发布 OINK 实现预览:它从 Docsy 直接演化而来,提供唯一标准产品外壳、仅依赖 Hugo 的消费端构建、本地优先浏览器依赖、通用多语言框架,以及一组从 PGSTY 文档站提炼出的可复用内容组件。
这是实现与文档里程碑,不是已经公开的版本化发行。最终公开品牌、模块与软件包身份、首个版本,以及生产 Cloudflare Pages 部署,仍是必须显式关闭的发布关卡。
为什么需要 OINK?
多个成熟文档站分别复制了相同的 Docsy 布局、导航、搜索代码、SCSS、JavaScript 与短代码。一个公共修复必须在多个仓库重复实施;与此同时,每个站点都携带前端工具链与隐含网络依赖,让网络隔离构建变得异常复杂。
OINK 将真正可复用的部分合并到主题中。产品矩阵、门户、价格页和其他业务专用行为仍留在各自站点;共享主题负责文档外壳、浏览器运行时、多语言路由、无障碍行为与内容组件契约。
有哪些变化?
产品本身,而不是一种模式
OINK 不是可选皮肤。项目没有 oink.enabled 开关、params.oink.*
命名空间,也没有“上游版与品牌版”并行的模板树。theme/ 中的实现就是产品。
这项决策避免维护两套视觉系统与两套测试矩阵。Hugo 原生设置与兼容的 Docsy 参数继续保持原有含义。
消费端仅依赖 Hugo 构建
完整消费站点只需运行:
Bootstrap、Font Awesome、字体、搜索、图表、API 文档运行时与 OINK 组件都已随主题提交。Node.js、npm、PostCSS、Autoprefixer 与 CDN 下载不属于消费端要求。
仓库维护者仍会使用 Node 工具运行测试和更新 vendor 资源;这套维护工具链有意置于公开站点构建契约之外。
本地优先的浏览器行为
默认 starter 会从生成后的站点提供页面外壳、字体、图标、搜索、Mermaid、KaTeX、Markmap、Swagger UI、Redoc、Asciinema、ECharts 与 Infographic 依赖。可选运行时按页面选取,并且每页最多加载一次。
PlantUML 与 Diagrams.net 不再拥有公共服务默认值。站点必须配置受控端点、使用预渲染结果,或明确选择远程服务。
多语言基础设施
语言路由来自 Hugo 的语言与翻译对象。配置一种语言时隐藏选择器;配置两种或更多语言时,直接点击按配置顺序切换,短暂悬停或键盘聚焦则打开完整菜单。当前页面缺少译文时,选择器会进入目标语言首页,而不是失效路径。
starter 与本站均以英文为首要语言、简体中文为第二语言。核心文档与博客范围内的每个页面都有并置的
.zh.md 译文,且标题采用稳定的显式 ID。
可复用组件
OINK 新增主题级 Asciinema、ECharts、Infographic、文档轮播、折叠块、标签页、卡片、导航卡片、文档卡片与参数组件。它们会生成唯一实例 ID,并且只在实际使用时加载本地资源。
ECharts 接受结构化 JSON 或 YAML,也支持通过 $fn:name
引用可选的 JavaScript 回调。回调代码只在声明它的页面运行。
哪些能力保持不变?
OINK 沿用 Docsy 的内容组织、front matter、文档与博客 section、菜单、taxonomy、打印输出、仓库链接、常用短代码、图表、API 参考能力与扩展 hook。现有站点可以删除重复公共实现,而不必重写普通 Markdown。
项目也保留 Docsy 的 Apache-2.0 历史与归属信息。vendor 清单记录固定的第三方来源、许可证、产物与校验值。
体验 starter
安装 Hugo Extended 0.160.1 或更高版本,然后在当前检出目录运行:
当前验证基线为 Hugo Extended
0.164.0。打开生成的英文与中文页面,切换语言、使用本地搜索、改变颜色模式,并访问组件示例。
需要传入网络隔离环境时,维护者可以创建完整归档:
归档包含主题、starter、许可证、上游记录、迁移指南、vendor 清单和配套校验值。
当前验证范围
当前实现的自动化覆盖包括:
- 最低版本与当前版本 Hugo Extended 构建;
- 禁止消费端 Node/npm/PostCSS/Autoprefixer 路径;
- LTR、RTL、子路径、打印、颜色模式与生产资源;
- 完整的一种/两种/三种/四种及以上语言选择器矩阵;
- 本地按页运行时与重复组件实例;
- ECharts 结构化选项与回调集成;
- 离线双语 starter 与离线发行归档;
- vendor 许可证与校验值;
- SILO、PGSTY、SOW 与 Pigsty 的非破坏性迁移演练。
最近一次四站演练成功构建了临时副本,但没有修改或部署这些生产仓库。
正式发布前还需要什么?
公开身份与首个版本必须获批,并一致应用到模块、软件包、源码标签、嵌套主题标签、归档与文档。随后还要把目标 Cloudflare Pages 项目连接到源分支,使用固定 Hugo 版本构建、公开发布,并在托管 URL 上完成验证。
这些关卡关闭之前,请把该预览用于评估与迁移演练,不要把未固定版本的代码当作生产依赖。