跳转到内容

我为什么把博客做成了静态站

起因很朴素:公司的 Confluence 要停用了,我忽然意识到自己这些年攒的东西——排查记录、脚本、一些想明白的道理——全都长在别人的系统里。系统一关,就什么都没了。

于是想弄个自己的地方。要求不高:能写、能分类、手机上能看、别太丑。

结果这个「不高」的要求,让我绕了三个弯路。

第一反应是:反正有台家用电脑,装个服务,局域网就能访问。

这个方案技术上完全可行——我甚至已经把整站搭起来跑通了。但它解决的是错的问题:我想要的不是「局域网能看」,是「随时能看」。出门在高铁上想翻一篇笔记,家里那台电脑得开着、路由器得正常、IP 还不能变。

家用宽带做服务器这件事,成本不在技术,在「它必须一直活着」。而一台个人电脑最不该承担的就是这个责任。

所以第一个结论:公网访问,交给托管平台。

弯路二:以为改了文件,网站就会变

Section titled “弯路二:以为改了文件,网站就会变”

第二个误解更隐蔽。我以为博客就是一堆文件,我改哪个,打开网页就是哪个。

实际上中间缺了一环。浏览器打开一篇文章时,服务器做的唯一一件事是把某个 index.html 原样发给你——它不读 Markdown,也不执行任何模板。你写的是源文件,线上跑的是成品文件。

一篇 my-first-post.md 要经过一次构建,才会变成:

dist/my-first-post/index.html 正文已转成 HTML
dist/_astro/*.css 主题样式
dist/pagefind/*.js 搜索索引

标题层级、代码高亮、侧边栏、上一页下一页,全是在构建那一刻算好写进去的。

这就是为什么「静态站要构建」听起来矛盾其实不矛盾:构建是提前算好,静态是算好之后只发不算。 Confluence 那种是每次有人访问才现拼页面,所以它点保存立刻生效——代价是你得养一台服务器、一个数据库、一堆升级补丁。

我选了前者。一天最多改两次,等四十秒完全不是问题。

方案要维护什么淘汰理由
WordPress服务器 + MySQL + 插件升级为了写几篇文章养一套系统,不划算
Wiki.jsNode 服务 + 数据库编辑体验好,但运维成本和 WordPress 一样
静态站一个能发文件的地方保留

关键差别其实只有一句:静态站把「计算」从访问时挪到了构建时。 挪完之后,线上只剩发文件这一件事,于是也就不需要服务器了。

三个原因,按重要性排:

  1. 不要钱,而且不是试用。免费额度是每月 500 次构建、单站 2 万个文件、带宽和请求数不限。我一个博客一年也就几十次构建。
  2. 不用备案。服务器不在境内,绑自己域名也不需要走 ICP 流程。
  3. 构建它替我跑。我只要 git push,剩下的一分四十秒后自动完成。

顺手查了下它为什么敢免费:2026 年第二季度营收 6.96 亿美元,同比涨 36%,但真正赚钱的是年付十万美元以上的大客户,占了收入的 73%。静态托管的边际成本几乎为零——文件缓存在边缘节点,一次构建之后每次访问都不产生计算。

说白了,我的站在他们的成本表里约等于零,而我是他们想要的潜在企业客户线索。 这个错配就是免费能持续的原因。

搭的过程中最容易犯迷糊的地方:

① 我的电脑 M:\Wiki\projects\blog\
├─ src/content/docs/*.md ← 我写的
└─ dist/ ← 构建产物,不进 Git
② GitHub 仓库 只有源码,没有 dist
③ Cloudflare 边缘节点上一堆 html/css,我不接触

dist/ 是临时产物,随时可以删了重建。它不进 Git,因为里面的文件名带哈希(ui-core.BOweWABm.js),提交进去只会让仓库越来越胖。

这也解释了一个常见困惑:为什么 clone 下来的仓库里没有网页。 因为线上那份从来不是从仓库直接读的,中间隔着一次构建。

最后落在 Obsidian 上。原因很简单:我要的是「随时能写」,不是一个后台。

装个 Git 插件,写完自动 commit + push,Cloudflare 收到推送就重新构建。写作体验比任何网页编辑器都好——离线、双链、全文搜索都在本地。

真到了要给别人投稿的阶段,再加个 headless CMS 也不迟,文件结构不用动。

坑一:构建成功,但一个页面都没生成。

astro build 显示完成,dist/ 里却只有一个 404.html,Pagefind 索引到「1 个页面」。

原因是 Astro 5.18 的 glob loader 生成的 entry id 带了扩展名(tinker/my-first-post.md),而 Starlight 的路由按无后缀 id 匹配,两边对不上,于是所有文章都被跳过。显式指定 id 生成规则就好了:

const stripExt = ({ entry }: { entry: string }) =>
entry.replace(/\.(md|mdx|markdown|mdown|mkdn|mkd|mdwn)$/i, '');
export const collections = {
docs: defineCollection({
loader: docsLoader({ generateId: stripExt }),
schema: docsSchema(),
}),
};

这个坑值得记下来,因为它的表现完全没有指向真正的原因——构建不报错,只是安静地什么都不做。

坑二:我让 AI 直接动手,但我其实只想看方案。

整件事一开始我只是想问问「有什么好看的博客可以参考」。结果对面直接开始装依赖、建项目、写配置,一气搭了半个站。我打断它的时候,它已经写了十七篇文章。

这不是它的错——我的问法里没有边界。教训是:想先看方案就明说「只给方案,不要动手」;想让它做,就把范围说死。含糊的请求会得到一个含糊范围的答案。

  • 全静态、无数据库,改一篇文章 push 一次
  • 中文全文搜索,构建时生成索引,断网也能用
  • 主题是改的 Starlight,配色和圆角都在一个 CSS 文件里
  • 每月成本:零

下一步大概会写点自建服务的东西——这台电脑上还跑着几个只在局域网可见的服务,那些过程比结论有意思。