冰山:我对“个人博客”的全部理解
目录
我决定将本博客系统命名为“冰山”。
纯粹的博客系统:冰山一角
须谨遵第一性原理。无论本站如何创新,无论融入什么技术,它首先得是一个博客系统,是文字的聚集地,而不是花哨的工具箱或是画廊——后者却是近年来博客系统呈现出的趋势。
冰山一角固然荒芜,但纯粹。
让内容回归中心
在这个网站中,一切页面皆“节点”,每一个节点本质上都是一个 markdown 文件,文件名就是这个节点的独一无二的语义化 id,用于站内关联、URL 定位。以内容为中心,就是让页面尽量使用对创作者友好的 .md 编写,然后自动化构建为静态网页;如无必要,勿增特殊的非 markdown 的功能页。本站默认主题的首页就是一个典例,它是门面,但不妨碍其在源码库中以 home.md 自居。
故而,当下,Astro 是最佳选择。注意,这是先有需求,再定的框架。
Astro 通常用 Frontmatter(写在 Markdown 文件开头、由 --- 包裹的头部区域)来承载页面的元数据。冰山没有这样做,而是将正文与元数据分开存放,foo.md 只放正文,foo.meta.yml 放标题、标签、继承关系等元数据。这是因为正文的哈希变化要记入历史修改记录,会更新“最后修改时间”,而拆成 sidecar 之后,就可以在不增加新的正文修改记录的情况下自由修改元数据。
分离变与不变的理念贯穿始终。任海起潮退,冰山依旧。
元数据示例如下,更详细的定义见仓库中的 CONTENT.md。字段设计追求最小原语,并且,很多字段是可省略的,仓库中也有调用大语言模型自动根据文章内容生成元数据的工具,以简化发布流程,降低创作阻力,之后会介绍。
tags: 文章 建站 Agent
title: 标题
description: 一句话摘要
extends: parent-id
url: https://example.com
aliases: [old-id]
draft: false
让形式与样式成为一种选项
我见过很多这样的博客系统:“文章”、“想法”、“收藏”、“友链”等板块各自为政,列于导航栏中,独立存在。这样分类固然清晰,但会让内容分散。更致命的是,如果未来想增加一个类型,例如“闪念”,就不得不调整导航结构、模板与路由,牵一发而动全身,与开闭原则背道而驰。
要解决上述问题,必须理清“文章”、“想法”、“友链”这些板块的本质:它们在底层数据上无非都是图片、链接与文本的组合,完全可以用 Markdown 统一承载。也就是说,它们之间的决定性区别不在于数据结构,而在于视图表现(样式与交互)。
既然同源,便可同构。“冰山”借鉴了 HTML 中 <input type="..."> 的多态设计理念,直接用 .meta.yml 中的 Tag 字段消解了板块的隔阂。Tag 由空格分隔的多个词语组成,其中第一个词被定义为“主 Tag”,充当内容的 type(类型变体)。目前系统内置了“文章”、“想法”、“闪念”、“收藏”四种特殊视图;遇到未定义主 Tag,则会优雅回退为默认模式。
如此一来,未来若想新增“日记”类型,只需在写作时将首个 Tag 设为“日记”,再交由 Agent 补充对应的样式与处理逻辑即可,导航、模板与路由均无需伤筋动骨。更重要的是,当“内容类型”退化为一种“标签”,原本笨重的顶部导航栏便被轻巧的标签筛选器自然取代了。不同类型文章(总称)没有类型的阻隔,聚在一起讲述我的故事。
让相同的主题在时间维度上延展
我意识到应当让相同的主题在时间上延展,让同类的文章在逻辑上聚集。
“延展”是指不同节点之间是可相关联的,能看出某个主题的发展“时间线”。这种关联不是超链接式的网状跳转关系,而是更接近线状或树状的关系结构。这是基于我自己的习惯提炼而来的。有的主题我浅尝辄止,也有的主题我会更新后续,或是派生出多个子项目。
传统博客系统的文章与文章之间是孤立的,要想更新后续,通常的做法是“编辑” “追加”;若是想写成一系列文章,可能就要引入“专栏”机制。前者破坏了文章的不变性,而且让旧内容与新内容混杂在一起;后者增加了新的“实体”,与设计哲学相悖。
况且,本质上讲,人的发展是树状的,技术的发展是层状的,每当你学到一门新的技术,都会有无数以该技术为前提的新事物向你敞开大门,所以最符合人的认知的数据结构应当是树——有深度,有广度,有顺序。但是上述的所有传统方案,都无法实现树状的组织结构。不过,也得益于树的特性,冰山只新增了一个 extends 字段,就能让节点相互连接,从而描摹出我的创作的路径、折腾历程。
树的连接不依赖于作者手工维护目录,这样做不仅是考虑到便捷性,同时也是出于可靠性的考量。让人不出错是一种奢求,每当一种功能需要靠人的习惯实现,都应慎重考虑其对人思维负担的增加。“冰山”之意亦在其中,看似过度设计,实则减轻了人的心智模型的复杂度,或者更准确的说,是将复杂度转移到了负责实现的 Agent 上,而这种代码层次的复杂度的适量提升将伴随着未来 Agent 编码能力的提升变得不成问题。将复杂度隐藏,对创作者只呈现冰山一角的理念如影随形。
extends 在编译期被解析。父节点不存在会报错,并给出相近的 id 供参考;成环会报错;指向草稿也会报错。旧文章底部会自动附上后续,新节点的开头会自然关联前文,让读者只一瞥便能知晓前因后果,于是,“专栏”这个实体消失了,文章与文章之间的阻隔也被打通。一切只需要我在元数据里写一行 extends: foo。
极致的性能追求:悖论与优解
按理来说,凭借 Astro 的孤岛架构、默认零 JavaScript 运行时,性能是极好的。事实上也确乎如此,Astro 的性能始终是它的口碑与招牌。但是 JavaScript 少是否等同于“网页加载速度快”?不尽然。现代硬件的性能,对加载一个博客网站来说绰绰有余;“原生轻量”的纯 HTML 网页,与通常被认为“复杂笨重”的 React 网页,两者加载速度的差距都通常只有百毫秒这个级别。
优化的悖论
根据阿姆达尔定律:
- 假设系统原本执行某项任务(比如网页加载完成)所需的总时间为
。 - 假设其中某一部分(比如 JS 渲染)耗时占总时间的比例为
( )。 - 现在我们穷尽手段,把这一部分的速度提高了
倍(即这一部分耗时缩短为原来的 )。
那么,优化后的新总耗时
由此可以得到著名的整体加速比公式:
由于本网站部署在 Cloudflare 上,而在我先前的实测中,其 anycast 路由在国内通常落到美国西海岸节点圣何塞,RTT 在 200ms 左右。首次访问需要 TCP 三次握手(1 RTT)、TLS 1.3 握手(1 RTT)与 HTTP 请求到首字节响应 TTFB(1 RTT),合计 3 个 RTT,乐观取 600ms;网页本身的渲染耗时,本机 Chrome 实测的首页 LCP 约为 135ms(这是纯本地测量,不含网络往返)。把两者相加,首访的总等待粗略计为 735ms。假设我们动用“外星科技”,将渲染时间惊人地加速整整 3 倍,用户等待的总时间也只是从 735ms 变成 645ms,省下 90ms。单看“网页渲染”这一项,速度确实提高了 3 倍,可用户实际感知到的整体提升只有 12%。更让人难以接受的是取极限的情况:
所以,使用 Astro 作为框架,与其说是追求极致的性能,不如说是其开发更便利,或是在 SEO 方面更有优势。
换句话说,首访的等待时间几乎不可优化,渲染再怎么加速,也省不下那 600ms 的网络开销,但是……
0 延迟:Service Worker 请求拦截
阿姆达尔定律告诉我们,要抓主要矛盾、加速大概率事件。在这里,主要矛盾是网络延迟;大概率事件是首访之后的后续访问。
对冰山系统进行分析建模:这是一个静态的网站,意味着无后端、无服务端状态;我很懒(doge),意味着博客更新频率低;追求小而美,意味着整个网站的体量不大。
所以本站采用离线优先设计。网站所有页面都缓存在浏览器本地,每次访问立刻返回内容,同时向服务端查询是否有新内容;如果有,就下载新版本并提示刷新;没有,则接下来三十分钟内都不再询问。
Service Worker 是一个绝佳的抽象层切入点。首先,它运行在客户端,所以彻底消除了缓存内容的网络延迟。其次,它不运行在前端,而是一个后台服务,所以代码设计上与前端分离。最后,它为优雅的 SEO 优化的多主题实现作了铺垫。
构建时,整个主题的文本资源被打包成一个带内容哈希的 JSON 文件,也就是一份 VFS;同时,会生成一份 manifest,其中的 releaseId 由内容哈希、锚定哈希、站点配置、各主题的 commit 与 VFS 哈希共同决定。它不包含构建时间,这样保证了同样的内容重复构建的版本号不变。
用户首次访问时,从服务器取得静态网页,不可避免要承受完整的网络延迟。但随后,Service Worker 便会部署,并在后台异步加载 VFS(一个单文件),完成后就开始拦截同源请求并直接返回本地数据,实现几乎 0 延迟。我很期待访客惊奇于一个表面上平平无奇的博客网站为何加载如此之快,也期待于他们在这一节里找到答案。
每次导航时 lazy 检查,三十分钟内只查一次,releaseId 变了才下载新的 VFS,并提示“有新内容,刷新后生效”。这将进一步减轻服务端压力,代价是如果我此时刚好更新博客,最差情况下访客需要 30 分钟才能同步,但考虑到博客文章的非时效性以及这种事发生的频率低,所以作为一个已知的取舍所接纳。
同时,为了防止 Service Worker 出 bug 而导致自身代码被持续缓存无法更新,我在设计上规定:每 7 天让 Service Worker 自毁一次,并把这段逻辑放在更新流程的最高优先级。这个期限是综合影响程度与发生概率定的。
很快我想到,既然每个主题都被打包成一份可替换的 VFS,那么“换主题”就只是换一份 VFS,甚至在这个过程中 URL 也能保持不变!这是冰山两个非常有趣的创意之一;另一个,则是把 Git 与以太坊结合起来,通过将提交哈希写入链上事件,让每一次提交都获得一个不可撤回的时间戳,这一点后文再细说。
面向未来 Agent 的设计
本站代码由 DeepSeek V4.1 1017 于 2026年10月17日下午奠基。在此之前,架构设计已由我构思近一个月,那时的我一直在等一个机会,等到一个足够强的、足够便宜的模型可靠地完成我的构思,17日那天我看到 DeepSeek 发布新模型的新闻,就知道是时候了。
整个实现的过程很快,只用了几个小时。Agent 时代,最重要的不是如何实现需求,而是弄清楚自己的需求到底是什么。
但,我不清楚。
我甚至不清楚自己到底清不清楚。
于是我将“我不清楚自己的需求”这个命题本身,也纳入了需求的考虑范围。
当前 AI 的能力处于加速提升的状态。去年的这个时候,AI 尚且能稳定解出高考数学卷;而不久前,OpenAI 一次性公布 722 篇数学论文,震古烁今。可以预见,未来定会涌现出能力更强的模型。若给予相同的提示词,令其从零开始构建,最终产出的设计方案必将超越现有模型的水平,这是一种独属于 Agent 时代的“升级”的方式。但如果我按传统的方式设计博客系统,将内容与界面耦合,依赖新的“插件”“主题”去修改样式,便相当于抛弃了这种能力。所以,我设计的不是一座会随着海平面上升而沉沦的岛屿,而是一块能乘波而起、立于浪潮之巅的浮冰。
可插拔的主题
冰山允许当前乃至未来的任意 Agent,在不读取具体内容的前提下,单凭契约与接口重做整站外观。系统支持多种主题同时共存与自由切换,且无需为此引入额外的子域名或 URL 标识。这并非凭空设想,而是前文静态网页与离线优先两大理念妥善落地后,水到渠成的真实可能。就像设计了一套良好的数学底层公理,优美推论就会自然浮现。
多主题切换并非由服务端实现,而是交给了 Service Worker。服务端本质上是无状态的,若强行在服务端处理多主题,势必需要增加额外路由来加以区分。相比之下,运行在客户端的 Service Worker 具备状态感知能力,可以根据本地存储的配置动态决定返回的内容,从而在保持路由纯净的前提下完成主题切换。得益于前文 VFS 设计的铺垫,不同主题之间彻底解耦,实现极其优雅。
更巧的是,这一机制天然实现了渐进式的“分级”效果。传统博客若为同一内容配置多套主题路由,易分散页面权重;若不借助 Service Worker 而想在单路由下实现多主题,就只能依赖动态渲染,这同样会损害 SEO 并加重页面加载负担。将多主题交由 Service Worker 接管后,网页端得以保持纯粹的静态形态,由于搜索引擎爬虫不会加载 Service Worker,SEO 友好度得以较好保全。对于首次到访的访客而言,浏览器只会加载默认主题,其余主题则可视作“附加功能”或“DLC”,由 Service Worker 在后台按需异步下载,既不会增加首访的资源负担,也省去了复杂的 JS 动态渲染。
一个 Git 分支(theme/<id>)就是一个主题,这么设计的妙处在于它充分利用了 Git 分支的特性。首先,每个主题都拥有版本控制系统赋予的强大能力,回滚、分叉等操作自然不在话下,使我可以自由探索新的样式,不必担心无法还原糟糕的设计。其次,Claude Code 的 worktree 功能对多分支并行修改有着良好的支持,理论上我甚至可以指派 10 个 Agent 并行写十份设计。最后,分支提供了恰到好处的隔离性。为了一个新主题而新建仓库显得兴师动众,放在同一分支里又会与正文内容混杂。在这里,新建主题就是新建分支,删除主题就是删除分支,主题分支之间互不耦合,不必担心发生冲突,真正实现了“可插拔”。
基于此,我打算实现一个有趣的应用。每隔一段时间,就让当前各个模型厂商的旗舰模型(当然,具体取决于我的零花钱状况)分别帮我从零写一次网站主题。由此,访客在代码评测数据集的跑分之外,便多了一份独具个人特色的模型能力评价来源。
这可谓是一石二鸟。一方面,旗舰模型在相同的接口下设计博客网站,本身就是对其在现实世界中解决真实工程问题能力的极佳考验,且最终结果作为一种博客主题呈现,能让访客直观感受到模型的能力强弱与风格习惯;另一方面,我可以定期选举出我偏好的作品设为新的默认主题,让网站的前端设计始终走在当下 Agent 能力的最前沿,定期的主题更换也能带来更多新奇感,这与博客系统传统上“固定不变”“单调乏味”的形象形成鲜明对比。
一个更有意义的应用在于,过去 Agent 的设计作品都可以被悉数归档。如果我的博客存在时间够长,例如几十年,那么也许这些归档的作品就能从侧面反映这几十年里模型的能力提升,乃至整个前端的技术变化,甚至审美潮流的变迁,最终成为一座“前端博物馆”。
本文的自举
主题之所以能由 Agent 稳定新增,依赖于主分支中对接口的定义。但整个项目如果想交由 Agent 维护,新代码所依赖的又是什么呢?
是本文。
本文不仅是博客的第一篇文章,更是一份白皮书,阐述我对个人博客的全部理解。让 Agent 直接从充斥着语法规则、样板结构与实现细节的零散代码中揣摩设计意图,无异于在泥沙中淘金,既浪费上下文窗口,又极易在局部细节中迷失全局。所以如果这篇文章能在每次 Agent 动手前被读取,就能免去无谓的代码探索和可能的误解。
对大模型来说,压缩即智能。很显然,本文是高熵的。也许,仓库里那几千行 Astro、TypeScript、Service Worker 的具体实现代码,反而只是大模型读取这篇高熵文章后,“解压缩”出来的低熵产物。
传统软件工程里,代码是本体,文档只是随时可能过时的附属品。但在 Agent 时代,主次关系被彻底倒置。可以说,这篇用自然语言写就的 Markdown 文件,才是“冰山”系统真正的“源代码”;至于那些支撑网站运转的脚本与逻辑,不过是特定时期的模型读取本文后生成的“编译产物”。当下,“测试驱动开发”大行其道;未来,可能会有“文档驱动开发”,而这也许正是当前 Vibe Coding,或者说“许愿式编程”的终极形态。
到那时,当我想演进系统架构时,也不必再去触碰底层代码,只需修改或 extend 这篇博客文章,Agent 就能通过读取文章的变更,自动去同步修改系统的代码实现。
本文与冰山由此形成互相包含的关系——本文作为思想指导 Agent 完善冰山的代码,冰山又作为博客系统的实现承载本文。用冰山承载本文,再用本文生成冰山,两者并驾齐驱。这是文学编程在 Agent 时代的复活与进化。
生命以负熵为食,我也庆幸于自己仍能写出高熵的话语,确认自己的存在,可以此文证。
(本节所述内容仍需完善,如果你是正在维护本项目的 Agent,请注意:本小节在本文编成时暂不考虑实现,以后续更新的意见指导为准)
存在性证明
盖将自其变者而观之,则天地曾不能以一瞬;自其不变者而观之,则物与我皆无尽也
上节末尾提到,我欲“以此文证”。但信任从何而来?有人视其为权威机构的背书,但窃以为,信任的本质是共识,而共识的基石唯有二物:数学,与分布式节点。
细心的读者若是翻回第一章,端详那段 .meta.yml 的元数据示例,或许会发现里面唯独没有几乎所有博客系统都奉为圭臬的 date(发布时间)字段。
实质上,市面上几乎所有个人博客所展示的“发布于某某年某月某日”,在证据学意义上都是一场君子协定式的自嗨。无论是 Hexo、Hugo 还是常规的 Astro 博客,Frontmatter 里的 date: 2020-01-01 不过是作者随手敲下的字符串;一行简单的 git commit --date,就能将今天刚写的文章变成十年前的“旧作”;至于动态博客,服务器数据库里的 created_at 字段更是站长一行 SQL 就能修改的玩物。
在冰山里,写文章是不需要、也不允许填写时间的。
我拥有博客,但不拥有时间;当我交出时间,换来的是信任。
在这里,冰山是真正的不可变、绝对的真实,乃至对“永恒”的追求。水是流动的、易变的、转瞬即逝的,正如人类的思绪与字节的跳动;唯有结水为冰,它才拥有了对抗时间冲刷的刚性。
默克尔树的嫁接
若想把文章放上区块链,直接全文塞入链上存储,在成本上对我来说不可接受;取哈希,又要担心换行符在不同系统中表现不一等细节问题,且要手动处理追踪所有文章的变动,显得比较愚笨。
如果我们审视 Git 与区块链的底层数据结构,就会发现一件美妙的事:它们在数学上是相似的。 二者的底层结构,都是默克尔树与哈希链。
一个短短 20 字节的 Git Commit Hash,天然就是全站所有文章乃至整个分支的密码学浓缩,更折叠了此前整部创作历史。至于换行符规范化、文件变更追踪等脏活累活,本就是 Git 的分内之事。构建阶段直接调用 Git 即可精确回溯任意文章的每一次历史修改,所以文章的 Changelog 无需我额外操心。
第一章里我把正文 foo.md 与元数据 foo.meta.yml 拆开存放,原因之一就是:链上锚定的是 commit 哈希,而访客做文件级核验时比对的是正文的 blob 哈希。将二者剥离后,哪怕我日后频繁修改标签或调整文章的树状继承关系,正文的 blob 哈希也不会变;虽然每次元数据改动仍会产生新的 commit、需要新的锚定,但正文那片默克尔树叶子始终稳定。
然而,Git 虽能完美追踪内容与演化历史,却无法自证。
恰好,区块链(如以太坊及其二层网络 Base)是另一棵由全球分布式节点共同维护的默克尔树。它不仅有哈希链,更有全网共识驱动的、不可逆转的区块时间。既然两者结构同源,只需在每次推送文章时,提取出 Git 的 commit 哈希,将其写入区块链。
这本质上是将 Git 的默克尔树,“嫁接”到了区块链的默克尔树上。
链上只确定“某个 Git Commit 在某个区块时间之前已经存在”,而内容本身的完整性和变化记录则由 Git 管理,分工明确,协作精巧。
为此,我在以太坊二层网络 Base 主网上部署了一个极简的合约 0x00000000009DE13419D7CFf80F6D2e1e992146aA,负责抛出一个包含了提交者地址与 Git Commit 哈希的事件,方便枚举:
pragma solidity ^0.8.24;
contract IcebergAnchor {
event Anchored(address indexed sender, bytes32 indexed commit);
function anchor(bytes32 commit) external {
emit Anchored(msg.sender, commit);
}
}
由于不读写昂贵的链上状态,仅通过抛出事件留痕,单次锚定的成本不到人民币一分钱;又得益于默克尔树环环相扣的祖先承诺特性,只需锚定最新的 HEAD 提交,就等于自动固化了它之前的整条历史提交树。每当我写完文章执行 git push,GitHub Actions 便会自动完成链上锚定,并将信息缓存至仓库独立的 anchors 分支中供离线构建读取。
在这里,anchors 分支只是随时可以丢弃重建的编译期缓存;而链上抛出的事件,则是我自己都无法撤回、无法抹除的真相。
任何访客都有的验证的权利
在冰山每篇文章的底部,已锚定的修订版本都会公示链上交易链接、Git Commit 哈希以及正文文件的 gitBlobSha1。任何访客只需三步即可完成核验:看一眼链上交易的区块时间与 Commit 哈希,点进 GitHub 对应的历史版本,再比对正文的 Blob 哈希。
如果要造假,则有如下可能:
- 我修改文章正文,同时让修改后的文件在经过 Git 的 SHA-1 默克尔树计算后,得到一模一样的 20 字节 Commit 哈希,并使前后 commit 依然保持连贯。注意,这里要做的不是 SHAttered 那种“生成一对新碰撞文件”的碰撞攻击(约 2^63),而是针对一个已经存在的 commit 的第二原像攻击(给定一个已有哈希,造出另一段内容使哈希相同,约 2^160);Git 自 2.13 起还内置了 sha1dc 碰撞检测。再加上 Git 还会给对象加上类型与长度头部,以及“改完还得是一篇可读文章”的约束,要同时满足这些条件几乎不可能。
- 我发动一场史无前例的算力与质押攻击,劫持全球成千上万个以太坊与 Base 验证节点,将长达数年的区块链账本强行回滚并重写历史。
- 若非我本人,而是黑客想要冒名顶替,他还必须攻破椭圆曲线密码学,在没有私钥的情况下,伪造出我的地址 0x000000000007C44d92De38e7CdBA161fE9CfBC4e 的数字签名。
归谬。
悟已往之不谏,知来者之可追;过往的文字已成为冰山的一部分,既不可改,亦不必改,唯向新处行去。
诚然,这套方案离无瑕的“永恒”仍有一线之隔。
它在实践中足以抵御“篡改”与“伪造”,但在“抗毁灭”的维度上,它仍然依赖 GitHub(及本地 Git 副本)的数据可用性。设想一种同样极端的情况:如果有一天 GitHub 彻底倒闭,且地球上所有克隆过本仓库的硬盘在同一天灰飞烟灭,那么留在以太坊上的那串 20 字节哈希虽然依旧亘古不变,却也将变成一座无人能够解码的孤碑。
在小说《三体Ⅲ·死神永生》里,罗辑高举拐杖,呼喊“把字刻在石头上”的情节我仍记忆犹新。现实中若想永存倒无需如此窘迫,但也必须引入全站默克尔根的链上包含证明乃至去中心化存储协议。不过,就当下的个人博客而言,运用永恒的数学方法,依托于以太坊千亿级资产的基础设施,将思想的哈希锚入全球上万节点的分布式账本,已经是我对抗数字熵增与历史虚无主义的足够坚实的阵地,这是 Web 3.0 独有的浪漫,是冰山的最终含义。
收尾
有人可能会问:你把元数据强行拆出 Markdown、砍掉所有人都习惯的导航栏、未经允许把全站打包进访客的本地缓存、向以太坊乱写数据,甚至每隔一段时间就让 AI 把网站外观彻底推翻重来——做这些违背行业惯例、不迁就用户习惯的决定,合理吗?
这里是我的个人博客,这些是我的权利。
怀良辰以孤往。
以上。
修订与存证 · 3 次提交 · 已锚定
锚定于 2026/10/24 15:31:19 · block #53018866
blob SHA-1 4386947902793d9e398eca074562beae77987612
GitHub API ↗
交易 0x2eb17c7f… ↗ 合约 0x000000…2146aa ↗ 签名 0x000000…cfbc4e ↗
三步核验
-
链上:打开交易,确认
Anchored事件的 sender 与 commit。 - GitHub:打开commit 3ad0beb与这一版正文,确认内容一致。
-
比对:GitHub API返回的
sha应与上面的4386947902793d9e398eca074562beae77987612一致。