这个小站上的每一份 Slides、每一篇文章,都是一个完整的、可以单独带走的 HTML 文件。样式和脚本全部内联,没有外部依赖。你可以把它丢进微信、拖进邮箱、拷进 U 盘,在任何一台有浏览器的机器上打开,看到的样子分毫不差。
这个约束听起来苛刻,带来的自由却实实在在:文件即作品。复制一份就是一次备份,发出去就是一次发布。没有「服务器挂了所以打不开」的焦虑,也没有「依赖的 CDN 变了所以版式崩了」的意外。
单文件是一种承诺:你看到的就是全部,全部都在你手里。
内容的组织方式朴素得近乎原始:一个目录就是一条内容。以 Slides 为例,decks/<slug>/ 里面只有两样东西——index.html 是作品本体,deck.json 是它的档案卡:标题、日期、类型、标签、阅读时长、可见性。
文章栏目沿用同一套约定,只换两个名字:档案卡叫 article.json,正文叫 content.html。名字变了,心智模型没变:看目录就知道站点上有什么,看档案卡就知道每份内容长什么样。
站点对外只暴露一份汇总数据:由构建脚本生成的 manifest.json。首页的「最新」卡片读它,列表页的筛选计数也从它来。档案卡分散在各目录里,汇总数据只有一个去处,两边永远不会各说各话。
把一份 HTML 放进站点,只需要一条命令:npm run decks:publish -- my.html --slug my-slug --title "标题"。简单归简单,脚本落盘之前要连续过四道闸,任何一道失败都会原样退出、一个字节都不写:
四道闸全部通过之后,脚本还会做一次起飞前检查:校验其它内容的档案卡是否完好。如果有哪张卡片已经损坏,这次发布会被拒绝——因为我绝不接受「发布成功、列表页却再也生成不出来」的状态。
列表页在构建时就完整生成:按年份分组的时间线、每张卡片上的日期、类型、阅读时长、摘要与标签,全部直接写死在 HTML 里。没有加载中的转圈,没有内容延迟到达时的抖动。
类型的筛选(「研究 / 图解 / 指南」那些胶囊按钮)交给一小段浏览器端的脚本完成:点击按钮,只是把时间线上不匹配的卡片隐藏起来。脚本不工作也没关系,完整的列表本来就在那里。先有完整,再有交互,顺序不能反。
整站是纯静态文件,部署在 Vercel 上:推一个分支,预览环境就出来了;合进主干,线上就更新了。仓库里那些与站点无关的旧项目目录,都被 .vercelignore 挡在部署产物之外——上线的东西,就是你在仓库根目录看到的那几样。
缓存策略同样直白:共享的样式表给五分钟的浏览器缓存,内容页每次都回源校验。样式可以稍旧一点,内容必须永远新鲜。没有构建流水线,没有打包产物,仓库里的字节就是线上的字节。
这套做法的边界同样清楚。内容到了几百篇,纯静态列表会需要分页;想要全文搜索,得另起索引;想让多人协作编辑,文件系统显然力不从心。这些需求都真实存在,只是它们属于另一个阶段。
在那之前,我宁可用两个不到三百行的脚本、一份清单文件和一堆单文件 HTML,换一个任何环节都能一眼看穿的小站。简单换来的踏实,睡得着觉的那种。