TigerBeetle 文档站生成器源码解析:从 Markdown 到 docs.tigerbeetle.com 的 Zig 构建流水线
【免费下载链接】tigerbeetleThe financial transactions database designed for mission critical safety and performance.项目地址: https://gitcode.com/GitHub_Trending/ti/tigerbeetle
本篇技术指南围绕 TigerBeetle 仓库中的文档网站生成器(位于 src/docs_website)展开,深入讲解它是如何把docs/目录与各语言客户端 README 的 Markdown 文件,通过zig build流水线转换为静态 HTML 站点、全站搜索索引与单页版本,并在发布时推送到 GitHub Pages 托管的 docs.tigerbeetle.com。读完本文,你将掌握该生成器的完整构建链路、目录树解析、链接检查、拼写校验与 CI/发布触发机制,并能据此本地构建和调试该文档站。
一、项目定位:一个用 Zig 实现的静态文档生成器
TigerBeetle 的官方文档站 docs.tigerbeetle.com 并不是由现成的 Hugo、Docusaurus 等工具生成,而是在仓库内部用 Zig 编写的一整套文档生成流水线。src/docs_website/README.md开宗明义地说明:
- 这是一个Documentation generator(文档生成器),目标输出是 docs.tigerbeetle.com;
- 静态网站通过
zig build生成; - 渲染结果被推送到独立的 docs 发布仓库,再由GitHub Pages托管上线;
- 也可以直接从仓库根目录用
./zig/zig build docs在本地构建(其中./zig/zig是仓库内托管的 Zig 工具链,见 zig/download.sh)。
整个生成器的源码集中在src/docs_website/目录,从源码结构看,它主要由以下模块协作完成(详见 src/docs_website/src):
| 模块文件 | 职责 |
|---|---|
docs.zig | 构建入口:遍历文档树、调用 pandoc、生成导航、输出页面 |
content.zig | 递归解析docs/目录结构与各 README,构建目录树(ToC) |
page_writer.zig | 把每个页面的标题、导航、正文套入page.html模板 |
html.zig | 极简模板引擎:$snake_case变量替换 |
single_page_writer.zig | 把全部页面合并成一个 single-page 版本并重写锚点链接 |
search_index_writer.zig | 汇总所有页面 HTML,输出search-index.json |
file_checker.zig | 后置校验:文件大小、类型、尾随换行、链接与锚点有效性 |
redirects.zig | 生成旧 URL 到新 URL 的 HTML 重定向页 |
website.zig | 公共配置(url_prefix、pandoc 路径、页面写出) |
二、构建入口与整体流程
从构建系统的视角看,文档站是以"嵌套构建"的形式接入主build.zig的:src/docs_website/目录会被作为独立的构建上下文处理(见 build.zig 与 build.zig 中对./src/docs_website/的 cwd 设置)。文档站自身在docs.zig的build()函数中组织全部流水线(见 src/docs_website/src/docs.zig),核心步骤如下:
- 解析文档树:以
docs/为根,调用content.load()递归加载所有页面(Page),并让每个页面持有其子页面; - 逐页转换:对每个页面执行 pandoc,把 Markdown(
gfm+smart-tex_math_dollars语法)转换为 HTML5,同时插入四个 Lua 过滤器; - 生成导航:基于目录树渲染左侧导航栏(
<details>/<summary>折叠结构),并高亮当前页面; - 组装页面:通过
page_writer把标题、导航、正文注入page.html模板,写出为<page_path>/index.html; - 构建搜索索引:把每个页面的 URL 与 HTML 交给
search_index_writer,汇总输出search-index.json; - 生成单页版本:
single_page_writer把所有页面拼接为一页,并把各页内链接改写成#slug-锚点形式; - 生成 404 页:把
html/404.html模板渲染为站点根部的404.html。
三、输入内容:docs/与各客户端 README
README 明确列出了两类输入:
docs/目录下的所有 Markdown(如 docs/README.md、docs/start.md、docs/coding/README.md 等子目录);src/clients/$lang/README.md,即各语言客户端的说明文档(C、Dotnet、Go、Java、Node、Python、Ruby、Rust,见 src/clients)。
有意思的是,客户端文档在最终站点中并不出现在它们源码所在的位置,而是被"搬移"到文档站的一个特定分类下。docs.zig的page_url()中有专门的特判逻辑(见 src/docs_website/src/docs.zig):
const url = cut_suffix(page.path, "/README.md") orelse cut_suffix(page.path, ".md").?; if (cut_prefix(url, "../src/clients/")) |client| { // Special case: docs for clients are in `/src/clients/$lang`, not under `/docs`. return std.mem.concat(arena, u8, &.{ "coding/clients/", client }) catch @panic("OOM"); }即src/clients/go/README.md会被映射为站点 URL 前缀coding/clients/go/...,与docs/coding/下的开发指南放在同一分类下,便于读者在同一导航树内找到语言客户端文档。
四、目录树(ToC)构建:content.zig
content.zig承担了"站点骨架"的解析工作。它把每个目录的README.md当作该目录的索引页,并从其中的列表链接(- 标题)递归地发现子页面,构建出Page树(见 src/docs_website/src/content.zig)。几个值得注意的解析规则:
- 子页面发现:只有形如
- title且以.md或/结尾的链接才会被识别为 ToC 子项(parse_page_child(),见 content.zig);链接必须以./开头,唯一例外是/src/clients/开头的绝对路径; - 孤儿页面检测:如果某个目录下存在既不是
README.md,也没有在任何 ToC 链接中出现的 Markdown 文件,构建会以error.OrphanedPage失败并打印orphaned page日志——这保证了站点导航不会遗漏任何页面(见 content.zig); - 显式排除:
internals/、TIGER_STYLE.md、ARCHITECTURE.md等不会被当作导航节点加载; - 标题解析:每页的 H1(
#开头)作为页面标题;客户端 README 开头的自动生成注释(<!--)会被跳过(见 content.zig)。
五、Markdown → HTML 转换:pandoc 与 Lua 过滤器
每个页面的内容转换由run_pandoc()完成(见 src/docs_website/src/docs.zig)。实际执行的命令等价于:
pandoc --from gfm+smart-tex_math_dollars --to html5 \ --lua-filter=pandoc/markdown-links.lua \ --lua-filter=pandoc/anchor-links.lua \ --lua-filter=pandoc/table-wrapper.lua \ --lua-filter=pandoc/code-block-buttons.lua \ --reference-location=section四个 Lua 过滤器分别位于 src/docs_website/pandoc,作用如下:
| 过滤器 | 职责 |
|---|---|
markdown-links.lua | 处理 Markdown 内链在 HTML 中的语义 |
anchor-links.lua | 为标题生成可跳转的锚点链接 |
table-wrapper.lua | 把表格包进可横向滚动的容器,适配移动端 |
code-block-buttons.lua | 为代码块添加复制按钮 |
转换结果通过--output=pandoc-out.html以构建产物形式传入后续步骤,确保整个流水线都受 Zig 构建系统缓存管理。
六、页面模板引擎:html.zig 与 page_writer.zig
页面组装基于两件事:一个自研的微型模板引擎和一个页面生成器可执行文件。
6.1$dollar_name模板引擎
html.zig实现的Html.write()用$snake_case变量替换代替常见的{curly_braces}语法——代码注释解释了原因:避免与模板内 JavaScript 函数语法产生歧义(见 src/docs_website/src/html.zig)。替换逻辑是编译期的:如果模板中引用了替换结构体中不存在的标识符,或替换结构体中存在模板未使用的字段,都会以编译错误失败(error.IdentifierNotFound/error.UnusedIdentifiers),从源头杜绝拼写错误。
6.2 page_writer:注入模板并计算脚本哈希
page_writer.zig是一个独立编译、以命令行参数驱动的可执行文件(见 src/docs_website/src/page_writer.zig),它接收标题、作者、url_prefix、页面路径、是否包含搜索框、导航 HTML 等 9 个参数,把内容注入 src/docs_website/src/html/page.html 模板。
其中有一个值得注意的安全细节:page.html中写入了Content-Security-Policy(script-src 'self' plausible.io 'sha256-$page_script_hash'),而页面脚本哈希是page_writer对实际注入的内联脚本做SHA-256 摘要并 Base64 编码后动态计算的(见 page_writer.zig)。这意味着内联脚本内容一旦变化,CSP 哈希自动同步更新,避免策略与脚本失配。
page.html模板本身(见 src/docs_website/src/html/page.html)还包含了完整的 SEO 与分享元信息:description、twitter:card、og:title、og:image、canonical链接、favicon 等,并在底部内联page-script.js与搜索脚本($search_script)。
七、左侧导航栏的生成逻辑
导航由nav_fill()递归生成(见 src/docs_website/src/docs.zig):
- 有子节点的目录渲染为
<details>/<summary>折叠项,子项递归填充;若当前目标页位于该节点之下,details会自动加上open属性展开; - 叶子页面渲染为普通
<li class="item">链接;若正是当前页,追加class="target"高亮; - URL 生成统一走
page_url():普通页面直接使用目录路径加尾斜杠,并拼接url_prefix。
这套逻辑保证了无论从站点哪个页面进入,导航树都能定位并展开到当前页所在的层级。
八、搜索索引:search-index.json
搜索能力由search_index_writer提供。docs.zig在遍历页面时把每个页面的站点路径与 pandoc 输出的 HTML 路径收集起来(search_index),随后以命令行成对参数喂给search_index_writer(见 src/docs_website/src/docs.zig)。该程序把所有(path, html)条目序列化为 JSON 数组并输出到标准输出,最终写为站点根部的search-index.json(见 src/docs_website/src/search_index_writer.zig)。
前端侧,src/docs_website/assets/js/search.js 负责加载该索引并实现站内搜索 UI;page.html模板中的$search_box与$search_results由 src/docs_website/src/html/search-box.html 与 src/docs_website/src/html/search-results.html 渲染,并为 single-page 模式关闭搜索框(include_search = false)。
九、单页(single-page)版本与锚点重写
single_page_writer把所有页面的 HTML 按序拼接到一个页面,输出single-page/index.html(见 src/docs_website/src/docs.zig)。拼接时它逐字符扫描 HTML 中的href与id属性(AttributeIterator,见 src/docs_website/src/single_page_writer.zig),并做两件关键转换:
- 链接重写:把
page_path + 相对链接解析为绝对 slug,形如#coding-debit-credit(路径中的/替换为-);带片段的目标重写为#slug-fragment; - ID 重写:为每个页面的锚点 ID 加上页面 slug 前缀,防止不同页面之间的锚点冲突;
- H1 特殊处理:H1 后紧跟的链接会被清空(
clear_h1_link),避免首页链接污染单页导航。
同时单页模式的导航不再使用多页 URL,而是通过url2slug()变为#coding-debit-credit/形式的页面内锚点(见 docs.zig)。
十、404 页面与 URL 重定向
- 404 页:
write_404_page()把 src/docs_website/src/html/404.html 模板渲染为站点根部的404.html,交给 GitHub Pages 在访问不存在的路径时展示(见 docs.zig); - 重定向:
redirects.zig内置了一张旧 URL → 新 URL 的映射表(如quick-start/→start/、about/→concepts/、about/vopr/→concepts/safety/、about/oltp/→concepts/oltp/),为每个旧路径生成包含<link rel="canonical">、<meta http-equiv="refresh">与 JSlocation跳转的 HTML 页(见 src/docs_website/src/redirects.zig),确保文档改版后旧链接依然可用。
十一、质量保障:file_checker.zig 链接检查器
README 特别强调"链接由./src/file_checker.zig检查"。file_checker.zig(src/docs_website/src/file_checker.zig)是一个独立验证程序,对zig-out生成的站点做多维度校验:
1. 文件分类与大小:按扩展名把文件分为 text(.css/.html/.js/.json/.svg/.xml)、binary(.avif/.gif/.jpg/.png/.ttf/.webp/.woff2)、exception(CNAME、.nojekyll)三类,出现未知扩展名直接报error.UnsupportedFileType;普通文件上限 166 KiB,search-index.json与single-page/index.html上限 2 MiB(见 file_checker.zig)。
2. 文本格式:所有文本文件必须以换行符结尾,否则报error.MissingNewline(见 file_checker.zig)。
3. 链接检查(check_links()):逐条扫描 HTML 中的href属性,检查:
- 协议安全:普通
http://链接会报error.InsecureLink(除非在http_exceptions白名单内),mailto:被忽略; - 冗余斜杠:含
//或/./的链接报error.RedundantSlash; - 目标存在性:相对链接以当前文件所在目录解析后必须存在(目录目标会自动补
index.html),否则报error.TargetNotFound; - 锚点存在性:带
#fragment的链接会在目标 HTML 中查找id="fragment",找不到报error.AnchorNotFound(见 file_checker.zig); - 外部链接:默认关闭实网探测(
check_links_external = false),但保留了两组针对 TLS 握手问题的https_exceptions白名单(如 kernel.dk 的 io_uring 论文等 PDF 链接),开启后可对白名单外的外部 URL 发起GET并要求返回 200。
这解释了为什么 README 说 CI 触发该流程"主要是为了检测坏链接":任何文档改版导致的死链或失效锚点都会让构建失败。
十二、拼写校验:vale 与 accept.txt
除了链接检查,构建流程还引入vale做拼写与风格检查。仓库内维护了一份接受词表 src/docs_website/styles/config/vocabularies/docs/accept.txt——技术文档中不可避免的专有名词(如 Zig、TigerBeetle、UInt128、LSM 等)都会登记在这里,避免被误报为拼写错误。新增技术词汇时需要同步更新该文件,才能通过 vale 校验。
十三、CI 与发布触发机制
README 最后点明了两条自动化触发路径:
- CI(
ci.zig):在合并队列(merge queue)中运行构建,主要目的是检测坏链接——也就是说,任何合并进主干的分支都必须能完整构建出文档站并通过链接校验; - 发布(
release.zig):在发版时执行构建并把渲染产物推送到独立的 docs 发布仓库,随后由 GitHub Pages 托管上线到 docs.tigerbeetle.com。
这与仓库中 src/scripts/ci.zig 与 src/scripts/release.zig 的整体 CI/发布体系保持一致,文档站构建作为其中的一个环节被调度。
十四、本地构建与调试
如果你想在本地构建并检查文档站,流程如下(仓库为只读状态,以下仅涉及本地构建产物):
# 从仓库根目录构建文档站(使用仓库内托管的 Zig 工具链) ./zig/zig build docs构建完成后,静态站点产物输出在仓库根目录下的zig-out目录中(README 明确写到 "Outputs are static HTML files in the./zig-outdirectory")。你可以直接用任意静态文件服务器预览:
python3 -m http.server --directory zig-out 8080若改动文档后构建失败,file_checker会给出精确的报错类别(TargetNotFound、AnchorNotFound、InsecureLink、OrphanedPage、FileSizeExceeded等)及出错文件路径,按错误类型修正链接、锚点或文件格式即可。
十五、设计要点小结
纵观整个生成器,可以提炼出几个值得借鉴的设计决策:
- 单一来源输入:目录树完全由各目录的
README.md的链接清单推导,配合"孤儿页面"检测,保证站点结构与文档仓库严格一致; - 构建阶段即校验:链接检查、锚点检查、文件大小/格式检查全部发生在构建期,坏链接在 CI 阶段就被拦截,而不是等部署后由用户发现;
- 零依赖模板引擎:自研
$dollar_name替换引擎配合编译期字段校验,避免引入重型模板框架的同时保证安全性; - 多形态输出:同一份 Markdown 同时产出多页站点、单页版(便于离线阅读与全文检索)与 JSON 搜索索引,三种形态共享同一套 pandoc 转换结果;
- 静态化部署:产物是纯静态 HTML + JSON,可无状态地托管在任何静态服务(本仓库选用 GitHub Pages),并支持 404 页与旧链接重定向。
对希望在 TigerBeetle 仓库中贡献文档的开发者而言,理解上述流水线意味着:新增页面时只要遵循"在父级 README 中以- 标题形式登记链接",并保证文中链接与锚点真实有效,其余(导航、搜索索引、单页、校验)都会自动完成。
【免费下载链接】tigerbeetleThe financial transactions database designed for mission critical safety and performance.项目地址: https://gitcode.com/GitHub_Trending/ti/tigerbeetle
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考