我们的博客不肯存下的那些词,以及这篇文章为什么写不出它们
2026-10-08 起,我们博客的新文章存进了数据库。新文章现在是表里的一行,不再是代码仓库里的一个文件;发布一篇文章是一次数据库写入,不再需要构建网站。这张表上有一个触发器:文章里只要出现一张短名单上的词,它就拒绝发布。
这篇文章也要过同一个触发器。所以它写不出那些词,也确实没写:不放在引号里,不放在代码块里,也不在字母之间加点或连字符。下面只按类别描述它们。这对我们不太方便,而这正是要点。
名单上有什么
名单共 10 项:
- 4 个英文形容词,是证书、徽章、印章上常印的那种:一个说有权威机构为它作保,一个说它受到保护,一个说它不会伤到你,一个说你可以放心依赖它。
- 1 个字母等级:成绩单上最高的那个字母,后面接英文里表示「等级」的那个词,中间可以是连字符、空格,也可以什么都没有。
- 4 个中文词:一个表示打分,一个表示通过的比例,一个表示健康的程度,还有一个意思是「全都没有问题」的四字短语。
- 1 个缩写:两个大写字母中间一道斜杠,表格里某一格不适用时填的那个。
英文条目按整词匹配,不分大小写,所以只是包含其中某个词的更长的词不会命中。中文条目在任何位置都匹配,因为中文词与词之间不留空格。那个缩写按整词匹配,而且只匹配大写。
这份名单不是专门为博客定的。它来自约束全站每个页面的文案规则,MDX 文章在构建时本来就有一个脚本按它检查。2026-10-08 改变的,是检查在哪里执行。
为什么把检查搬进数据库
存在数据库里的文章不经过构建,所以检查必须搬到写入发生的地方。触发器在这张表的每次插入和每次更新之前运行。草稿不检查,这样我们可以先写进去、读出来、再修改。一行以已发布状态写入,或者一篇草稿正被改成已发布时,它的标题、描述和正文都要过一遍名单。一旦命中,触发器抛出一个写明规则名的约束错误,什么也不会写入。网站使用的数据库角色只能读已发布的文章,别的都不能做;它们没有一个能写这张表。
一份名单,两种语言,一组测试字符串
名单只在一个 JavaScript 模块里定义一次。MDX 检查导入它,数据库里的一个 SQL 函数存着一份副本,有测试逐条比对两边。
名单相同还不够,因为两种语言匹配文本的方式不一样。词边界最明显。JavaScript 的 \b 只认 ASCII,所以紧挨英文词的汉字算作边界。Postgres 的 \y 把汉字也当作词的一部分,所以如果 SQL 用了它,一个前后紧贴汉字、不留空格的英文词就会漏过去。因此 SQL 从不使用 \y,而是用显式的 ASCII 字符类把 JavaScript 的边界写出来。
为了抓住这类差异,我们只维护一组测试字符串,让它在两种数据库排序规则下分别过两个检查器,结论必须完全相同。这组字符串一开始有 36 条,现在有 68 条,覆盖大小写、中文、词边界、包含名单词的更长的词,以及换行。
两次迁移上线之前,我们还在生产库上用一个最后回滚的事务跑了这组字符串:第一次 36 条全部一致,第二次 68 条全部一致。第二次的第一遍出现过 1 条不一致,原因在我们自己的工具:它在传输途中替换了那条测试字符串里的一个字符。按原样重发之后,68 条全部一致。
浏览器会删掉的换行
浏览器对东亚文字遵循一条 CSS 规则:两个宽字符(比如汉字或日文假名)之间的单个换行,在页面上会消失。所以 markdown 源文里一个被拆到两行的两字中文词,读者看到的仍是一个词。最初版本的检查匹配的是原始文本,看不到这种情况。代码审查在任何文章存进去之前发现了它。
第二次迁移之后,两个检查器都会先删掉一段空白:它恰好只含一个换行,而且两侧都是宽字符。两个换行表示新段落,保持不动。拉丁字母之间的换行显示为空格,所以那里什么也不拼接。
页面渲染时再查一遍
网页渲染一篇文章时,渲染器会再查一遍,这次查的是读者真正看到的文字:实体已解码,强调标记已合并,不可见字符已去掉,全角字母已折成普通字母。在这里命中,页面就返回 404,不显示文章。
渲染器的代码注释写明了两道检查都堵不上的一个缺口:名单词里夹一个来自另一种字母表的形近字母。所以能把这种拼法挡在这篇文章之外的,只有我们自己不用。检查可能放过的其他花招也一样。为了讲守卫而绕过自己的守卫,守卫就没有意义了。
没有豁免
MDX 检查允许豁免:某一个文件里的某一句原文,单独审过才能列入。目前有一处。我们那篇为什么不给 MCP server 打分的英文原文里有一个短语,点出了我们不提供的东西,为的是说明我们不提供它。
数据库没有豁免名单,触发器的报错信息里也写明了这一点。我们把那篇文章译成中文、作为第一篇存进数据库时,它和别的内容一样过了触发器。这一篇也是。
为什么博客也要守这条规矩
MCP Checkup 不给分,也不下判定。每项检查的结果是四种状态之一:已通过(Verified)、未通过(Failed)、观察到风险(Observed risk)、未检查(Not checked),每一行都附有依据。未签名的观测不着色,也从不呈现为签名结论。结论不能购买;要让这句话一直成立,最简单的办法是根本没有一个总结论可卖。
我们自己的网站上出现一个判定用词,就会把这些都抵消掉。如果这里有一篇文章用那几个形容词之一来形容一台 server,任何人都可以把它当作我们的结论引用,而任何报告上都没有一行能为它作证。那个缩写也在名单上,原因相近:我们没能执行的检查记为未检查,并附上记录下的原因;两个字母加一道斜杠,什么原因也没给。
博客是我们写得最自由的地方,也因此最容易混进一个随手的形容词。所以在报告页成立的规矩,在这里同样成立,并且由数据库在发布那一刻执行,哪怕文章是我们自己写的。
我们怎么数的
10 项名单、36 条和 68 条测试字符串、1 处豁免,都是 2026-10-08 在我们的仓库里数的:共享的名单模块、存放测试字符串的测试文件、MDX 检查的豁免列表。36 条和 68 条全部一致,是 2026-10-08 对生产库做的两次干跑的结果,每次都在对应的迁移应用之前。