周五下午 4 点,内网文档站上线前的最后一次检查。银行安全团队发来三行红字:mermaid 图表引用的公共 CDN 已被防火墙拦截;搜索框后面连的 Algolia 需要出网审批,流程两周起步;公式的字体来自 Google Fonts,想都别想。你转身想本地构建一版全离线的,结果新同事的
npm install卡在 node-gyp 上——他的 Node 是 18,项目要 20。一个文档站,什么正事还没干,先被工具链和外部服务按在地上摩擦。
如果我告诉你,有个开源项目,0 个 npm 依赖、不连任何 CDN、断网也能搜索、画图、渲染公式,连喂给 AI 的 llms.txt 都顺手生成好了——你信吗?
它叫 Oink,猪叫声的那个"哼哼"。
一、文档站的三座大山
写文档的人只想写文档,但 2026 年搭一个文档站,你实际要面对的是这个:

第一座山,工具链绑架。 主流文档框架都是好东西,但它们把文档站变成了一个前端工程:装依赖、锁版本、配编译。文档团队里往往没人专职管这个,于是每次升级都是一次冒险。
第二座山,外部服务依赖。 公共 CDN 在内网环境等于不存在,搜索服务要出网审批,字体服务看运气。你以为自己在"写文档",其实是在替第三方服务做可用性运维。
第三座山最隐蔽:内容锁死。 你的 Markdown 进去,出来的是一堆带导航、带侧边栏、带埋点脚本的 HTML——甚至很多框架要求你在 Markdown 里直接写组件,内容变成了某个前端框架的私有方言。读者想干净地复制一段?想把这本文档打印成手册?想让 AI Agent 好好读一遍?全都得费劲。
有没有一个项目,同时把这三座山搬走?
二、OINK 是什么?

Oink 是 Pigsty(企业级 PostgreSQL 发行版)作者Vonng推出的开源文档框架——用他自己的话说,不只是一个 Hugo 主题,而是一套文档发行版。
名字的来历很直白:一边是文档与墨水的 Ink,另一边是 Pigsty 宇宙里小猪的叫声 Oink。README 里那句 "Open. Indexed. Navigable. Knowledge." 就当是附赠的彩蛋。
一句话定义:只用 Markdown 和一个 Hugo 二进制——不要前端全家桶——统一构建文档、博客、书籍、发布页、Landing Page 和 API Reference,输出给浏览器、打印机和 AI Agent。
这个项目最硬的背书不是任何宣传语,而是作者的迁移清单:从 Pigsty 的中英文档、Silo、PIG、SOW、PG Exporter,到公司首页、个人博客,再到《设计数据密集型应用》(DDIA)这样的多语言书籍——18 个生产网站,全部跑在同一套框架上。

从上千页的大型技术文档到只有几页的小工具,从博客到书籍到 Landing Page,全部运行在 Oink 上。
用 Vonng 自己的话说:一个作者对工具最高级别的评价,不是 README 里写了多少卖点,而是敢不敢把自己的生产环境全部押上去。现在他押了。
三、开箱效果展示
说再多架构,不如直接看效果。下面这些截图全部来自作者自己在维护的生产站点,不是特意摆拍的 Demo。一套框架,六种内容形态,共享同一套设计语言、搜索、语言切换和输出体系。
3.1 文档: 页内纲要

左侧目录树、面包屑、右侧页内纲要一应俱全——右侧那条带进度高亮和移动圆点的纲要轨道,是主题自研的 outline 运行时(SVG 轨道 + clip-path 高亮),不依赖 Bootstrap 的 scrollspy。
3.2 博客:沉浸式 Hero 长文

0.6.0 引入的博客形态:全幅头图打底、作者档案署名(冯若航 / Vonng / Pigsty)、系列文章与标签体系。列表、卡片、表格三种索引形态可以随读者喜好切换。
3.3 发布与下载:数据驱动的 Release 页

一份结构化 YAML 数据,自动生成发布卡片、版本历史、各平台安装命令、下载资产列表和 SHA-256 校验和。发布状态不再散落在手写 HTML 里,而是可检查、可复用的数据。
3.4 Landing Page:服务端渲染的产品首页

内置一组服务端渲染的首页区块:Hero、功能板块、指标、FAQ、行动入口,全部用数据和 Markdown 组合。Oink 官网和 PGSTY 公司首页,本身就是用这套 Landing 系统搭出来的。
3.5 内容组件:原生 Markdown 的写法与效果

步骤列表的上半部分是渲染效果,下半部分就是它的源码——一个普通的有序列表,加一行 {.steps} 属性。离开 Oink,它仍然是任何 Markdown 工具都能读懂的列表。

文件树同理:渲染出来是带文件类型图标、对齐注释、可折叠目录的交互组件;源码是一个接近纯文本的缩进围栏。这就是"能用原生 Markdown 表达的,绝不发明新语法"落到实处的样子。
四、凭什么是它?跟主流文档框架掰手腕
先说 Oink 的定位。它不是"又一个漂亮主题",而是把整套文档交付问题打包解决:内容怎么组织、文档博客书籍如何共存、搜索导航多语言怎么做、同一份内容如何输出给浏览器打印机和 AI、网站如何构建检查部署升级。

跟主流选手逐项对比:
| Oink | ||||||
|---|---|---|---|---|---|---|
| Hugo | ||||||
| ❌ | ||||||
| ✅ | ||||||
| ✅ 内置双引擎 | ||||||
| ✅ 零 JS | ||||||
| ✅ 逐页判定 | ||||||
| HTML+打印+MD+llms.txt | ||||||
| ✅ 内置 | ||||||
| ✅ 中文全审校 | ||||||
| 原生 Markdown 优先 |
看清楚格局了:功能密度上它对标 Docusaurus 这类"全家桶",交付形态上它比 Hugo 原生主题还干净,内容哲学上它比谁都固执——能用原生 Markdown 表达的,绝不发明新语法。
这点值得展开。步骤列表在 Oink 里就是一个普通有序列表加一行属性;文件树就是一个接近纯文本的围栏块;Callout 用普通引用块,字段说明用普通表格。离开 Oink,这些内容仍然是任何 Markdown 工具都能读懂的文本。 用 Vonng 的话说:框架总会过时,内容应该活得比框架更久——样式控制不应该大面积侵入内容。
五、源码深度拆解
5.1 架构全景
Oink 的核心设计一句话就能讲完:能挪进构建时的,绝不留给运行时;运行时确实需要的,只给用得上的页面。

注意那个紫色的运行时层——这是 Oink 和几乎所有文档主题拉开差距的地方,下面细拆。
5.2 核心模块速览
layouts/_markup/ | ||
layouts/_partials/ | ||
assets/js/ | 6108 行,零框架零转译 | |
assets/scss/ | ||
assets/third_party/ | ||
bin/ | 33 个 Python 检查器 | |
i18n/ | ||
tests/ |
内置的内容组件覆盖了工程文档的全谱系——图表、公式、终端录像、思维导图、文件树、图廊,以及断网可用的 Swagger / Redoc API 文档:

这些能力全部按需加载:页面没用到的组件,一个字节的运行时都不会进页面。
5.3 标志位驱动的按需加载:图表不是全局背锅的
为什么一个只有几页图表的文档站,要为每一页付 mermaid 的加载成本? Oink 的答案是一个两段式机关。
第一段,渲染钩子在渲染元素时顺手登记标志位——mermaid 的钩子只有一行有效代码:
{{ .Page.Store.Set "hasmermaid" true -}}<pre class="mermaid"> {{- .Inner -}}</pre>页面里出现过 mermaid 围栏,Page.Store 里就多了一个 hasmermaid 标志;没有出现过,标志就不存在。
第二段,scripts.html 在页面尾部按标志位拼装 JS 包(节选):
{{ $jsArray := slice -}}{{ if $hasPlantuml -}} {{ $jsArray = $jsArray | append (resources.Get "third_party/pako/pako_deflate.min.js") | append (resources.Get "js/plantuml.js") -}}{{ end -}}{{ if $shell -}} {{ $jsArray = $jsArray | append (resources.Get "js/docs-shell.js") -}}{{ end -}}{{ if $hasTabRuntime -}} {{ $jsArray = $jsArray | append (resources.Get "js/tabs.js") -}}{{ end -}}{{/* 同一组合的页面共享同一个包:文件名是成员清单的 md5 */ -}}{{ $bundleKey := "" -}}{{ range . }}{{ $bundleKey = printf "%s|%s" $bundleKey .Name }}{{ end -}}{{ $js := . | resources.Concat (printf "js/page-%s.js" (md5 $bundleKey)) -}}{{ if hugo.IsProduction -}} {{ $js = $js | minify | fingerprint -}} {{/* 生产环境:压缩 + 指纹 + SRI */ -}}{{ end -}}三个设计决策值得品:
包名是成员清单的 md5。十个页面用到同样的特性组合,就共享同一个缓存文件——按需加载的常见做法是每页一包,Oink 用内容寻址把缓存命中率捞了回来。 1MB 的 ECharts 被刻意排除在拼接之外。源码注释原话:放进拼接包里,"每种标志组合都会背上一个无法缓存的兆级文件"。大文件单独指纹发布,小文件灵活组合。 生产环境全资产强制 SRI(Subresource Integrity):每个 <script>和<link>都带 sha256 完整性校验。静态站做到这个级别,供应链攻击面基本焊死。

5.4 双引擎搜索:Lunr 管英文,自研扫描器管中文
英文文档主题的搜索,到中文这里为什么集体失灵? 因为 Lunr 这类倒排引擎靠分词工作,而中文的词之间没有空格。Oink 的解法简单粗暴:检测到 CJK 字符,直接切换到自研的子串扫描路径。
搜索引擎的分流入口只有一行:
query: function (query) {return CJK.test(query) ? queryCjk(query) : queryLatin(query);},CJK 路径是一个手工权重的多字段扫描器(节选自 search-engine.js):
// 命中位置决定分数:标题最贵,正文最便宜var textScore = (titleAt >= 0 ? 100 : 0) + // 标题命中:100 分 (keywordAt >= 0 ? 80 : 0) + // 作者定义的关键词:80 分 (headingAt >= 0 ? 50 : 0) + // 小节标题:50 分 (descAt >= 0 ? 30 : 0) + // 页面描述:30 分 (bodyAt >= 0 ? 10 : 0); // 正文:10 分if (!textScore) return;// 最终分 = 文本分 × 页面权重(front matter 里的 search_boost)hits.push({doc: doc,excerpt: excerpt, // 命中处前后各截 24/56 字符做摘要score: textScore * number(doc.boost, 1),});两个魔鬼细节:
大小写折叠只做一次。源码注释明说: queryCjk每敲一个键都要扫全库,如果每字符都重新 lowercase 一份文档拷贝,就是每键一次全量内存分配——所以建引擎时一次性折叠完存起来。boost 的校验偏执到防 NaN:front matter 里的 search_boost只接受有限正数,+Inf、NaN、负数一律打回并降级为 1.0,带 warning。
英文路径则走 Lunr:精确词 boost 100、前后通配 boost 10、再容错编辑距离 2——错拼一两个字母照样搜得到。索引本身是构建时按语言生成的 JSON(字段、摘要长度、范围全部可配),生产环境带 md5 指纹,整个搜索链路没有一个字节来自第三方。
入口是一个 Ctrl+K 的命令面板:/ 进全量搜索、> 进命令模式,WASD/jk/qe 单键导航,焦点陷阱和 IME 输入法组合态都处理了——你用中文输入法打第一个拼音的瞬间,它不会把按键误当快捷键。
5.5 公式在构建时渲染:数学页面的"零 JS"特权
为什么公式要等浏览器加载 KaTeX 再渲染? Hugo Extended 的二进制里已经编译进了 KaTeX,Oink 直接在构建时把公式变成成品 HTML:
{{- $opts := dict "output" "htmlAndMathml" "displayMode" $display -}}{{ with try (transform.ToMath .Inner $opts) -}} {{ with .Err -}} {{ warnf "KaTeX 渲染失败,公式保持原样:%s" . -}} {{ else -}} {{- .Value -}} {{/* 成品 HTML + MathML 双输出 */ -}} {{ $.Page.Store.Set "hasMath" true -}} {{/* 只登记 CSS 字体需求 */ -}} {{ end -}}{{ end -}}注意那个 hasMath 标志:数学渲染完 HTML 之后,运行时只需要 KaTeX 的 CSS 和字体文件——JS 一行都不用加载。渲染失败也不炸,公式保持原文并打 warning,这就是它"每个开关都优雅降级"哲学的一个切片。
markdown 和 RSS 输出里公式则退化为编号文本(**式 1.**),打印视图保留图表编号与交叉引用——同一份内容,多种受众多种形态。
5.6 四种输出格式:给浏览器、打印机、读者和 AI 各留一扇门

每个 HTML 页面都会声明自己对应的 Markdown 地址,而那个 .md 版本不是把 HTML 反向转换一遍,而是保留你真正写下的内容——Callout、表格、文件树、代码围栏都按各自的 Markdown 形态输出:

同一个 URL 后缀换成 .md,拿到的就是干净的源文档——组件退化回围栏,公式退化回 LaTeX。
llms.txt 的生成逻辑很克制:菜单里的外链(仓库、聊天室)被明确排除——源码注释说它们是"chrome, not content",列进 AI 索引只会稀释内容。给 AI 的饲料,纯度比数量重要。
5.7 质量工程:16682 行 Python 在守门
这是全项目最反常识的部分:检查器代码(1.67 万行)比模板实现(1.17 万行)还多。CI 里跑着 33 个检查器,其中最狠的是"四态 golden 比对":
用 181 页的回归夹具构建出 HTML / 打印 / Markdown / RSS / LLMS 的 40 个黄金输出面,归一化指纹哈希和机器路径后逐行 diff 锁定——任何一个输出字节变了,CI 都会让你解释。此外还有 85 项迁移测试、38 项浏览器运行时测试,外加双语站点构建、大型站点性能测量和真实中英文浏览器检查。
CI 矩阵跑 Hugo 0.160.1 和 0.164.0 两个版本,全程 --panicOnWarning——警告即失败。还有一个专门的测试:用 Hugo Module 模式构建一个消费方站点,确保"模块发布形态"和"本地主题形态"的路径解析都被覆盖。
错误策略也分了场景:hugo server 遇到错误配置尽量警告并降级,不让一个错别字搞挂整个预览;生产构建则严格panic。这套思路很像数据库系统——开发阶段给诊断,生产发布立门槛。
版本哲学同样是一景。0.6.1 的更新日志修了一个 1 像素的导航栏错位,讲清楚了 border-box 高度、50px 预留带和基线对齐的来龙去脉,写了三段论文式的分析。这种 changelog,说实话,比多数项目的 RFC 都认真。
六、典型场景
场景一:政企内网/离线环境的文档站

银行、政务、军工、工厂车间——这些环境里"文档站功能阉割"是常态,搜索砍掉、图表砍掉、公式变乱码。Oink 的产物是一个纯静态目录,扔进 nginx 或者任何文件服务器就完事,审计时每一个字节都可以本地核对。Pigsty 这种发行版软件的私有化交付文档,就是这个场景的重度用户。
场景二:写书、翻译、维护大型双语知识库
Oink 的"书籍"内容形态是为长篇内容专门做的:章节编号、图片与表格编号、公式编号、交叉引用、目录索引、整本书连续打印。Vonng 拿《设计数据密集型应用》(DDIA)这样的多语言复杂书籍当主要试验场:

带编号的图、表、公式和交叉引用——这是长篇技术内容真正会遇到的问题。
多语言直接用 Hugo 原生模型,翻译文件与原文并排放置;英文、简体中文、繁体中文界面文本完整维护;中文搜索原生可用;博客形态则带作者档案、系列文章、沉浸式 Hero 长文页面。
场景三:把文档喂给 AI Agent

2026 年,文档的第二读者已经是 AI 了。Vonng 的判断更激进:Markdown 已经成了人类与 AI Agent 之间的公共协议——README、AGENTS.md、设计文档、知识库,最终都落在 Markdown 里。Oink 的内容模型从一开始就假设:这份文档既要给人看,也要给机器读。
不想从零开始?抄作业
Oink 的公开案例库收录了 15 个真实生产站点,从两页的小工具到上千份内容文件的发行版手册都有覆盖。作者推荐的最快路径不是从空目录研究配置,而是找一个最像你需求的现成站点,克隆下来,把 content/ 换成自己的。

这些不是专门做出来的 Demo,是作者自己每天都在维护的生产站点。
七、5 分钟跑起来
前置条件只有三样:Git、Go、Hugo Extended ≥ 0.160.1。对,没有 npm install,没有 node_modules,没有 lockfile。
方式一,从零初始化:
# 1. 初始化一个站点模块hugo mod init github.com/example/docs# 2. 拉取 Oink 主题hugo mod get github.com/pgsty/oink@latest方式二(官方推荐),直接克隆样例站改内容:
git clone https://github.com/pgsty/oink.pgsty.com my-docscd my-docshugo server然后在 hugo.yaml 里启用它(关键部分):
module:imports:- path: github.com/pgsty/oinkoutputs:home: [HTML, RSS, markdown, LLMS] # 按需开启四种输出page: [HTML, markdown]section: [HTML, RSS, print, markdown]params:offline_search: true# 本地搜索,构建时生成索引ui:image_zoom: true# 图片点击放大dark_mode:show_menu: true# 暗色模式切换菜单本地预览:
hugo server# 大型站点想跳过编辑循环里的索引构建:HUGOxPARAMSxOFFLINE_SEARCH_ON_SERVE=false hugo server部署更是无聊:hugo 一条命令,把 public/ 目录扔上任何静态托管——GitHub Pages、Cloudflare Pages、对象存储、内网 nginx,都行。
从 Docsy 迁移的用户,仓库里带了 bin/check-site-markup.py 迁移体检脚本和 85 项配套迁移测试,先体检再动手。甚至可以按作者的建议,直接把建站任务整段扔给 Claude Code 或 Codex——文档站源码本身就是完整样例,Agent 照着抄就行:

作者给出的原始提示词:描述你想要的网站,Agent 负责克隆样例、改配置、换内容。你只负责两件事——告诉它你要什么,以及把真正有价值的内容写出来。
八、谁不适合 Oink?
公平起见,把丑话说在前面:
1. 你要的是营销官网,不是文档。 Oink 是内容优先的框架,Landing 能力只是内容壳的延伸。复杂产品官网请左转 Astro / Next.js。
2. 你的文档要嵌 React/Vue 交互组件。 它坚持零框架原生 JS、构建时渲染、原生 Markdown 优先,MDX 那种"文档里写组件"的玩法是它刻意排斥的方向——用 Docusaurus 或 Fumadocs。
3. 你要的是在线协作 Wiki。 这是 Git 工作流的静态站,没有拖拽 CMS、没有多人在线编辑后台、没有账号权限系统。它的边界很清楚:以 Markdown 和结构化数据为源,构建静态内容网站。
4. 你需要一个"稳"字当头的成熟生态。 这是它目前最大的短板:项目只有两周大,0.x 版本,单一维护者。API 会动、配置键会改(它自己的迁移检查器就是干这个的)、bus factor 是 1。生产使用务必 pin 死版本号,并且接受"升级要读 changelog"的代价。
5. 你的团队完全不在乎工具链重量。 如果站点永远跑在外网、团队养得起前端工程师、也没有内网合规需求——那 Docusaurus 的生态红利确实更香。
九、结语
回到开头那个周五下午。用 Oink 重来一遍:hugo 一敲,产物里 mermaid、搜索、公式、字体全部自包含,防火墙随便拦,Algolia 是谁?然后你把 llms.txt 的链接甩给正在写运维 Agent 的同事——他不用写任何爬虫和清洗逻辑。

Web 开发这十几年,钟摆一直在"瘦客户端"和"胖客户端"之间荡。Oink 代表的是一次干脆的反向压缩:把前端工程化十五年攒下的运行时,尽量塞回构建器的一个二进制里——图表构建时渲染、公式构建时渲染、搜索索引构建时生成,浏览器只拿走它真正需要交互的那部分。
而在钟摆的另一头,它还给下一个时代的读者——AI——预留了整整齐齐的餐桌。人类看 HTML,机器读 Markdown,一本内容两种形态。
这个项目当然还年轻,但一个人十三天写出 11 个版本、测试代码比实现代码还多、敢把 18 个生产网站全部押上去——这种工程习惯比任何宣传语都值得信任。
用作者的话收尾:把 Markdown 喂进去,剩下的,交给这头猪。
地址:https://github.com/pgsty/oink
官方文档:https://oink.pgsty.com
案例库:https://oink.pgsty.com/case/
夜雨聆风