如果你的博客和我一样,只是一个人的阵地,那大概率也和我一样,被那些花里胡哨的“全功能”后台编辑折磨过。我独立维护 lizizai.xyz 这个中文技术博客,最让我心累的不是琢磨技术,而是每次写完一篇文章,都要面对一套繁琐的发布流程。
我总想着,能不能把博客写进我每天都在用的文档工具里?写完就完事儿,什么发布按钮、什么 commit push、什么服务器,统统不用管。
听起来有点异想天开,对吧?毕竟传统的博客,要么像 WordPress、Ghost 那样,你得自己养台服务器,配个数据库,然后登录后台,在那个功能齐全但总觉得像在办事大厅填表的编辑器里码字,最后点个“发布”按钮,文章才算上线。要么就是像 Hugo、Jekyll 这类静态网站生成器,你在本地用 Markdown 写,写完保存、git commit、git push 到仓库,等 CI/CD 流程构建、部署,文章才出来。改个错别字?抱歉,全套流程再来一遍。
我每天处理工作,写文档、做笔记、开会协同,用的都是飞书。它的协作、评论、版本历史、多端同步,都让我觉得顺手。手机上改个错别字,电脑上接着写,草稿躺在那边,随时可以打开继续。我特别希望我的博客文章也能“长”在飞书里,写完就完事,多余的发布动作一个都不要。
所以,我把自己的博客系统彻底“推倒重建”了一遍。现在,我的 lizizai.xyz 博客,已经彻底告别了传统意义上的服务器——没有 VPS,没有数据库实例,没有 Nginx 配置。文章直接在飞书里创作和管理,写完什么都不用做,它就能自动同步上线。而且,它还支持中英双语,一篇文章可以有文字、播客、幻灯片、交互 HTML 四种形态,点赞、评论、浏览量这些互动功能也一个不少。
今天这篇,就作为《无服务器博客搭建记》系列的第一篇,跟你聊聊我的心路历程,我是怎么从对各种后台编辑器和发布流程的不满,一步步走到现在这个完全“飞书驱动,无服务器”的博客架构的。
删出来的架构:从 Strapi 到飞书
在现在的架构落地之前,我也走过不少弯路。
这个博客项目最早的时候,用的是 Strapi。如果你不熟悉,它是一个 Headless CMS,通俗点说,就是给你提供一个管理内容的后台和一套 API 接口,让你存文章、图片这些数据,但它不负责把这些内容渲染成页面。页面部分需要你自己搭。我当时用 Next.js 16(一个 React 全栈框架)来渲染前端页面,内容则从 Strapi 里拿。
现在回头看,我的代码仓库里还留着 scripts/migrate-strapi-to-mdx.ts 和 lib/content.ts 这两个“遗迹文件”,只剩下 Git 历史还能考古它们曾经的存在。是的,后来我把 Strapi 全删了。
原因很简单:一个人维护的博客,Strapi 太重了。它要求你额外管理一台服务器、一个数据库,定期还得操心升级、备份这些运维工作。而且,它的写作后台虽然功能强大,但对我来说,真的不如每天免费用的飞书顺手。我发现,我为了用它而付出的运维成本,远远超过了它给我带来的便利。
这个架构不是我一开始就“设计”出来的,而是我在实践中,不断地“删”出来的。每一次删除,都是基于同一个判断:这东西带来的麻烦是不是超过了它省的事?如果答案是肯定的,那就毫不犹豫地删掉它,寻找更轻量、更高效的替代方案。
我还考察过一些其他的“文档即发布”产品,比如 NotionNext、Super.so 等等。它们的核心思路和我的想法很接近,都是在 Notion 里写文章,然后自动生成网站。这个方向是没错的,但问题在于,整个网站的命门都押在单一平台的 API 上。比如 Notion API 就有限流,网站加载速度慢,有时候还没办法优化;如果平台改接口、调价格,我的网站就跟着晃,没有安全感。
不选这些产品,不是它们不好,而是我不想把自己的博客完全绑定在别人的平台规则上。加上我在国内,飞书比 Notion 用起来更顺手,这是我最终选择飞书的全部理由。
我的无服务器博客架构:一家报社的比喻
在经历了反复的试错和删除之后,我现在这套无服务器博客系统才逐渐成型。为了让你更容易理解,我喜欢用一家报社来做比喻,这样能把各种技术组件的角色对应起来。
整个链路是这样的:
- 飞书文档:编辑部
- 这里是我写文章、修改、和自己(或者未来的编辑)协作、评论的地方。所有的草稿、终稿,都“长”在这里。飞书强大的版本历史功能,也完美解决了文章回溯的问题。
- GitHub Actions:取稿快递员
- GitHub 的 CI 服务,简单理解就是一套在云端跑脚本的服务。它每天早上会定时“上班”,去飞书编辑部里,把有变化的稿件取出来。
- Cloudflare R2:仓库
- 这是一个对象存储服务,你可以把它想象成一个巨大的文件仓库。快递员取回来的稿件,会在这里被转存成 Markdown 文件,文章里用到的图片也会一并保存,同时还会生成一个总目录文件
articles.json。
- Next.js on Vercel:印刷厂
- Vercel 是一个前端托管平台。当有新内容进入仓库时,它会触发构建,从 R2 仓库里拉取最新的内容,然后用 Next.js 这个框架把它们“印刷”成读者能看到的网页。它利用了 ISR (Incremental Static Regeneration,增量静态再生) 技术,页面渲染后会被缓存起来,所有读者共享这份缓存,直到缓存过期或被手动刷新。
- 边缘服务 (Cloudflare Workers):读者服务台
- 这是四个独立部署的边缘服务(跑在 Cloudflare Workers 上),负责处理读者与网站的互动,比如点赞、评论、浏览量统计、访问分析等。读者浏览器会直接与这些服务台打交道,而不需要经过“印刷厂”。
为了让你更直观地看到这个数据流向,我准备了一张图:
你可能注意到了,“取稿快递员”和“印刷厂”中间隔了一个“仓库”。这是我故意设计的解耦。快递员只负责把飞书里的内容搬进 R2 仓库,印刷厂只负责从 R2 仓库里拉内容。这样做的好处是,即使同步器(快递员)出了问题,网站也能照常读取旧内容,读者访问不受影响。
内容和互动分走两条线也是同样的道理:文章就像是印刷好的报纸,而点赞评论这些,则像是读者写来的信件,直接寄到读者服务台处理,不影响报纸的发行。这种分离让整个系统更健壮,各个部分可以独立更新和维护。
快递员的上班时间:GitHub Actions 定时取稿
在整个链路中,GitHub Actions 扮演的“取稿快递员”角色至关重要。它确保了飞书中的最新内容能够被及时同步到我的内容仓库 R2。我设置了一个定时任务,让它每天固定时间“上班”:
这里 cron: '0 1 * * *' 是一个五段式的时刻表,分别代表分钟、小时、日期、月份、星期。0 1 * * * 的意思就是每天 UTC 时间 1 点运行,换算成北京时间就是上午 9 点。这样,每天上班前,我的博客内容就能自动同步一次。
同时,我还留了一个 workflow_dispatch 的手动触发入口。写了急稿,或者想立即看到修改后的效果,不用等到第二天早上 9 点,可以直接在 GitHub 页面上点个按钮,或者通过 gh workflow run feishu-sync.yml 命令手动触发一次同步。这就像是给快递员留了个“加急件”通道。
与其他方案的对比:我为什么选择这条路?
聊完了我的方案,我们不妨拿它跟市面上常见的其他博客搭建方式做个对比。这能让你更清楚地看到,我的选择到底解决了哪些痛点,又带来了哪些独特优势。
| 维度 | WordPress / Ghost(自托管) | Hugo / Astro + Git(纯静态) | 我们的方案(飞书 + 无服务器) |
|---|---|---|---|
| 服务器 | 要一台 VPS 跑 PHP/Node + MySQL | 不需要,产物是纯静态文件 | 不需要,全部是托管服务 |
| 写作环境 | 自家后台编辑器 | 本地编辑器写 Markdown | 飞书文档 |
| 发布动作 | 后台点"发布"即生效 | commit + push 触发构建 | 什么都不做,等定时同步 |
| 日常运维 | 升级、备份、防攻击都要管 | 只管 Git 仓库 | 没有可运维的东西 |
| 数据在哪 | 自己的数据库 | 自己的 Git 仓库 | 飞书 + 自己的 R2 存储 |
这张对比表可以归结为两条线:要不要养服务器,发布要不要自己动手。
第一条线:要不要养服务器?
对于一个团队来说,管理一台或几台服务器可能不是什么大事。但对于像我这样独立维护博客的个人来说,这绝对是一个持续的负担。我的博客可能三个月不更新都没事,但一台服务器三个月不管,那可能就出事了——系统漏洞、服务挂掉、磁盘空间满,各种麻烦事都可能找上门。WordPress 或 Ghost 这类方案,即使有托管服务,但自己掌握服务器的感觉,往往也伴随着无尽的运维烦恼。
而纯静态网站,比如 Hugo,它的产物是纯静态文件,不需要服务器运行环境,直接丢到 CDN 上就能跑。这已经很接近无服务器了。我们的方案更进一步,彻底把所有的后端服务(内容存储、构建、互动)都交给了托管平台,你不需要关心任何一台服务器,甚至连配置一个 Nginx 都省了。
第二条线:发布要不要自己动手?
这是我选择飞书作为写作环境的另一个核心原因。
WordPress 等平台的“后台点发布”虽然直观,但你还是得登录后台,面对那些表单字段。而本地写 Markdown 的方案,虽然写作体验本身很纯粹,但写完之后,保存、git commit、git push、等待 CI/CD 构建部署,这一套流程下来,往往会打断我的写作心流。尤其只是改个错别字,也要重复一遍,就更让人觉得麻烦了。
我的方案,核心就是“什么都不做,等定时同步”。我只管在飞书里把文章写好,写完就关掉文档。剩下的同步、发布,都交给自动化流程。这真正做到了写作和发布的解耦,让我可以专注于内容创作本身。
零月账单与真实代价
聊到这里,你可能会好奇,这样一套听起来这么先进的系统,每个月得花多少钱?
答案是:月账单为零。
- GitHub Actions: 对公开仓库来说,它提供的分钟数是免费且不限量的,我的同步脚本完全在免费额度内。
- Cloudflare R2: 有非常慷慨的免费额度,个人博客的内容量根本塞不满。
- Vercel: 提供了免费的 Hobby 档,个人博客使用绰绰有余。
- Cloudflare Workers: 我的边缘服务跑在上面,同样有免费额度,个人博客的访问量也完全够用。
所以,从金钱上看,这套系统是“免费”的。但天下没有免费的午餐,它的真实代价是什么呢?
首先,搭这套系统花的时间远比直接买一台 VPS 多得多。 这是一次性的投入,我投入了大量的精力去研究、调试、打磨每一个环节,才能换来后续每个月零账单和接近零的操心。一旦搭好,维护成本极低。
其次,也有一些权衡和妥协:
- 发布有小时级延迟: 我的同步是定时触发的,文章在飞书里改完到真正上线,会有几个小时的延迟。对于我这种个人博客来说,通常不是问题,因为没有时效性那么强的需求。急稿可以通过手动触发同步解决。
- 文章同时活在飞书和 R2 两处: 这两份内容需要通过同步器来保持一致。虽然目前的同步器运行良好,但这始终是一个需要维护的逻辑。
实践中的两个“坑”
这套系统跑了这么久,自然也踩过一些坑。
第一个坑,是 GitHub Actions 的 cron 定时任务。 前面说了,我设置的是每天 UTC 01:00 运行。但实际上,GitHub Actions 的官方文档不保证 cron 任务的准点执行,尤其在高峰期,任务可能会排队,导致滞后几十分钟到数小时都是正常的。有那么几次,我发现第二天早上 9 点多我的博客还没更新,一查 Actions 运行记录,发现是任务排队了。
所以,我把它当成一个“兜底”机制,而不是精准的闹钟。如果文章有任何时效性要求,或者我急着上线,我一律会选择手动触发(在 GitHub 网页上点按钮或者跑 gh workflow run feishu-sync.yml 命令)。定时同步更多是作为一个常规检查和更新的手段。
第二个坑,和网站的缓存策略有关。 有一次,我眼睁睁看着 GitHub Actions 报告同步成功了,R2 仓库里的 Markdown 文件也确实更新了,但我的博客首页和文章页就是看不到新内容。我反复检查同步日志,没问题啊!后来才发现,问题出在 Vercel 的 ISR 缓存上。Vercel 会把我的页面缓存一小时,即使内容源更新了,它也不会自动失效,同步成功后读者可能还得等上一小时才能看到最新内容。
解决办法是,在同步成功后,我的 GitHub Actions workflow 会额外调用站点上的 /api/revalidate 接口。这个接口的作用就是主动清除 Vercel 上的 ISR 缓存,让它立即重新生成页面。当然,这个步骤即使失败了,也不会阻塞整个同步流程(最坏情况就是读者晚点看到,缓存一小时后也会自己过期),所以它是一个非关键的优化。
这个经历也让我明白了,使用托管平台时,一定要搞清楚它的缓存策略,因为它会影响你的内容更新速度。托管平台的行为是它的,不是你的,你得去适应它,或者通过它提供的 API 去控制它。
好了,这就是我搭建这套“飞书驱动,无服务器”博客系统的初步故事。它解决了我的核心痛点:告别服务器运维的重担,以及繁琐的发布流程。让我可以把全部精力投入到最有价值的事情上——写内容。
在下一期,我会深入探讨一个更具体的技术细节:我的博客里有 163 篇文章,快递员每天怎么知道哪几篇改动过,然后只取这几篇,而不是把所有文章都重新同步一遍?这里面涉及到一个“高水位”检查点的概念,以及一个容易踩的 mtime 陷阱。敬请期待。