WSA(7)

NAME

wsa - 本站的构建与部署架构

SYNOPSIS

posts/*.md
  -> src/markup.rs  解析一次,产出事件流
       ├─> src/html.rs  首页、文章页
       ├─> src/roff.rs  manual page
       └─> src/feed.rs  RSS
  -> public/
  -> rsync
  -> /var/www/thinker

零外部依赖。读者收到静态文件。

HISTORY

这套架构改过三次。每一次都不是为了更强,而是为了更少。

第一代用 Pandoc、template.htmlfeed.xml.sh 和一个 Makefile 把 Markdown 拼成网页。它能工作,但它是一组彼此知道太多的小脚本:模板知道文章长什么样,脚本知道模板长什么样,改一处要跟着改三处。

第二代把这些收进一个 Rust 生成器,用 pulldown-cmark 解析 Markdown。边界变清楚了:作者写 Markdown,生成器出 HTML 和 RSS,服务器发文件。但它留下一个没被看见的失衡——自有代码三百多行,依赖三万五千行,而那三万五千行里为本站服务的不到一成。

第三代删掉了那个依赖。

DESCRIPTION

现在的判据是:整个二进制里的每一行都算本站的代码。

按这个口径,pulldown-cmark 是最大的冗余源。它实现完整的 CommonMark,而本站四年十四篇只用到十种构造,且全部是平的——没有嵌套列表,引用块里只有段落。

于是文法收窄成一套封闭语法,由 src/markup.rs 自己实现。

这不是"一个不完整的 Markdown 解析器",是"一个小语言的完整实现"。区别在于边界是否可论证。

GRAMMAR

文法的边界由"手册页是什么"决定,不由用量决定。

roff 是一个有主见的目标格式:.SH.SS.IP.EX.TS 各自对应一件确定的事。超出这套宏的构造,在手册页这个概念里没有位置。

例如标题只到三级,不是因为没用过第四级,而是因为 roff 只有 .SH.SS 两层。

文法之外的写法不会被静默渲染,而是构建时报错,并指出文件和行号。

posts/2026-07-26-x.md:42: 不支持嵌套列表

这条让"没有冗余代码"从口号变成可判定的性质:每段解析代码对应一条产生式,没有产生式对应的代码就是冗余,可以删;反过来,语法之外的输入不会悄悄走到任何代码路径上。

完整清单在 AUTHORING.md (external) (external)。

GENERATOR

九个文件,一千三百行,零依赖。

markup.rs 是唯一懂语法的地方。它不知道 HTML,也不知道 roff。

两个后端各自消费同一份事件流。解析发生一次。

这是加第三个后端只需要新增一个文件的原因,也是这套代码没有互相纠缠的原因。

OUTPUT

public/
  index.html
  YYYY-MM-DD-*.html
  feed.xml
  style.css
  assets/
  man/man7/*.7
  man.tar.gz

manual page 不是比喻。装上之后 man thinker 可用,man -k thinker 能检索全站——后者是意外收获:每篇文章的 NAME 分区本来就是按手册体例写的,apropos 直接就能用。

BUILD

make check 依次做六件事:格式检查、跑测试、构建、校验 RSS、用 mandoc 校验全部 manual page、确认生成 HTML 里没有 <script

最后一条现在其实是多余的——裸 HTML 在新文法下是硬错误,进不来。留着是纵深防御。

测试有一百一十一个,其中包含提交进仓库的黄金文件:一份覆盖全部构造的样例,和它在两个后端下的预期输出。渲染结果一旦变化,测试就会失败。

之前"输出与旧实现字节一致"只是手工验证过一次,仓库里没有任何东西守着它。现在有了。

同样的输入永远得到同样的输出——生成器不读时钟,lastBuildDate 取自最新一篇文章的日期,而不是构建时刻。

SERVER

服务器只托管 public/

它不理解文章,也不负责构建。它只是发送文件。

CI/CD

推送到 main,GitHub Actions 跑 make check,通过后用 rsync 部署。Pull Request 只检查不部署。

NOTES

三代下来,真正变化的不是能力,是边界的清楚程度。

第一代的复杂度散在脚本之间,谁都知道一点别人的事。第二代把它收进一个程序,但把三万五千行外部代码当成了地基。第三代把地基也拆了——不是因为那些代码不好,而是因为它们为本站服务的部分不到一成,其余九成永远用不上。

窄规则没有变过:

复杂度属于作者和构建过程;读者只接收安静的静态文档。

变的只是"复杂度"这个词涵盖多少。第二代认为依赖不算,第三代认为算。

SEE ALSO