这个网站背后的核心技术,是 PHP、SQLite 和 Twig。页面生成后写成 HTML 文件,由 Nginx 直接提供。能读文件,就不必每次都跑 PHP。

这套东西谈不上新。用了这么多年,我仍觉得合适。原因也简单:大部分精力应该花在产品上,而不是不断照看开发工具。

但「用熟悉的技术」不等于「一直不管它」。这套选择也有成本,有些还正好是因为它太省心,才被拖了下来。

先说它怎么工作

iHomePage CMS 从 2017 年前后开始做,重写过几次。核心是 PHP 8、Nginx、PHP-FPM、SQLite 和 Twig 3。一个站点一个数据库文件。

内容先用 Twig 渲染,再写到磁盘。访问者通常拿到的是静态 HTML。文件不存在,才交给 CMS 处理。后台保存、全站构建、手动重新生成,以及动态兜底之后的落盘,都走同一个发布方法:SiteService::publish(url)。

这里最有用的决定,是把生成页面和提供页面分开。模板渲染慢一点,未必影响每个访客。我们有个站点的 sitemap 构建,要遍历 35 万行,约 11 秒,内存峰值约 8MB。放在定时任务里可以,放在每次请求里就不行。

再简单一点说:即使 CMS 今晚停了,已经生成的文件还在,Nginx 仍能继续提供页面。这一点让我很踏实。

熟悉,指的是知道它怎么坏

Dan McKinley 在《Choose Boring Technology》里用了「创新额度」的说法。一家公司能承担的新东西有限,别把额度都花在基础设施上。

这个提醒有用。但对我们这样的小团队,我更关心的是:五年以后,这个东西是不是还得由我和身边几个人来解释、维修?

很多技术很好,我也不想接。并不是怕新,而是不愿再多一件长期要照看的事。

这里的「熟悉」因人而异。做惯 Kubernetes 的团队,可能就觉得它平常。PHP 和 SQLite 对我省心,是因为我了解它们。没这个前提,照抄我的技术栈没有意义。

三个决定,确实省了很多事

第一,内容模型没有不停长大。

文章、商品、栏目、标签,都放在 post 表里,用 taxonomy 区分。层级用 parent_id,多对多关系放在 post_post。增加一种内容,通常加一个类型名和模板就够了。

这些年模型还少了一张表。原来单独存栏目的 term,后来并进了 post。对我来说,这比又抽象出一套复杂体系更有价值。

第二,数据库是文件。

一个站点,就是站点目录和 SQLite 数据库。备份可以用 VACUUM INTO。迁移不用再搭数据库服务、导入数据、补权限。运行中的数据库要按正确方法备份,不能因为「只是文件」就随便复制,但整体要管理的东西确实少。

第三,日常访问不用依赖整套动态系统。

PHP、查询层和模板负责生成文件,Nginx 负责送文件。每一层做的事都比较清楚,出了问题也容易缩小范围。

SQLite 不是不用学

WAL 可以让读操作和一个写事务同时进行。它不等于允许多个写事务一起跑。写入者仍然只有一个。

这个 CMS 会设置 busy_timeout。可能需要写入的事务,也要考虑什么时候取得写锁,不能读到一半才发现锁升级失败。大批量构建还要控制事务批次,别把前台和后台的写入一直堵住。

这些都是真知识。SQLite 界面简单,不代表使用条件也会自动消失。我觉得它省心,有一部分原因是这些功课已经做过了。

Tailscale 排查过 SQLite WAL-reset 导致的数据损坏。漏洞潜伏多年,修复进入了 2026 年发布的 SQLite 3.53.0。写这篇时,我检查了 PHP 链接的版本,仍是 3.40.1。

这对我更像一个提醒:选了可靠的项目,还得确认自己实际跑的是哪个版本。发行版替你打包,并没有替你决定所有风险。一个没有 CVE 编号的数据损坏修复,也可能值得优先处理。

越不坏,越容易忘记升级

频繁出问题的技术栈,会逼着人升级。一直能跑的系统,反而容易年年往后拖。

PHP 有支持期限,前端库也有。Bootstrap 4 从 2023 年起就不再由上游提供安全更新。旧版本今天还能显示正常,不代表它还在维护。

写这篇时,页面前端还有 jQuery 3.6.0、Bootstrap 4.6.2 和 Popper 1.16.1。静态博客里的输入路径少,某些风险暴露面相对有限。但这不能变成所有系统继续使用旧库的理由。换成允许用户输入内容的应用,判断就变了。

甚至不能只说「我们用 jQuery」。项目还在维护,和你安装的那一版还在维护,是两回事。

这类技术栈需要一个主动安排的维护日期。没人逼你,不代表可以一直不做。

有些规则,只写在人的脑子里

PHP 8 的参数类型、返回类型和严格模式,比早年的 PHP 好很多。但它们替代不了所有检查。

代码里曾有个 post.info 字段,里面既放过 PHP 序列化结果,也放过 JSON。后来统一让结构化数据进 *_json 字段,旧字段却仍能被写入。于是规则还是「别这样用」,没变成「这样用不了」。

有些错误,类型检查也抓不到。一次静态发布重构,漏掉了动态渲染后的落盘步骤。网页还是正常的,只是每次都重新运行 PHP。类型没错,行为少了一步。需要检查发布结果,或者有相应验证,才能发现。

不过这也不能拿来替缺少静态分析开脱。写这篇时,仓库里没有 PHPStan,也没有 Psalm。既然知道工具能帮忙,至少应该从较低级别开始用,而不是让「熟悉」变成一直靠记性。

手写 CSS 和招人,都有上限

个人网站用一份 CSS,完全可以。组件少,写的人少,命名冲突还能控制。到了几百个组件、多人一起改,维护成本就不同了。

招人也是一样。我觉得顺手的技术,未必是别人愿意接手的技术。有些团队看到 jQuery 就会排除。PHP 的熟练开发者很多,但培养新一代维护者,也是长期问题。

如果打算明年招十几个工程师,我现在的选择未必合适。它和公司刻意保持小规模,是连在一起的决定。

新东西,我也用

同一个 CMS 里有 MCP,也有 OAuth 授权。我们还在做大模型和 RAG 的产品。我没有觉得互联网应该停在十几年前。

关键在于,新东西放在哪里。

MCP 走通用的模块路由。核心不需要为它认识一套特殊规则,授权数据也放进现有的 JSON 字段里。如果以后不做了,可以撤掉模块,不必先拆掉半个 CMS。

我希望数据模型、存储、渲染这些基础部分少变。需要试的新功能,放在容易替换、容易移除的地方。基础稳定,才有精力尝试新东西。

我会提醒自己什么

  1. 加新技术前,先问现有工具能不能解决。
  2. 能把页面生成和访问分开,就先分开。
  3. 用 SQLite,先学清写入、锁和备份,再说它简单。
  4. 多个服务要高并发地写同一份数据,就重新评估数据库,别硬撑。
  5. 记录实际依赖版本和停止支持日期,定期检查。
  6. 补上静态分析和关键行为的验证,别把经验当成全部保障。
  7. 新功能尽量做成可拆的模块。接进来之前,就想好以后怎么拿掉。
  8. 招人和交接也算成本,别只比较运行速度。

我们当然也有构建流程。它负责发布、记录文件归属、处理批量事务。我能说的只是:这套流程比较小,自己读得懂,也知道它大概会怎么失败。

我选技术时越来越常问的是:五年后,我还愿意管它吗?基础部分少一点麻烦,剩下的精力才轮得到真正想做的产品。