把一个 SPA 博客补成可预渲染、可同步、可持续部署

Date 2026-05-29 · Category blog · Status finished · Confidence likely
系统排障, 工程实践

这次不是做“新功能”,而是把站点补成能长期运行的状态

这个博客原本已经能用,但存在几个典型问题:

  • 它是一个 SPA,页面内容依赖前端运行后才出现。
  • WebFetch 抓不到渲染后的正文,SEO 和分享预览都不稳。
  • /news 是每天持续增长的数据,但本地仓库和线上站点并不总是一致。
  • 服务器可以生成日报,却没有稳定地把新内容自动写回仓库。

结果就是,页面表面上都能打开,但一到构建、预渲染、同步和部署这些真实流程里,就会开始出现“本地没问题,线上不一致”“今天能发,明天可能覆盖”的问题。

预渲染与部署收口封面图

这次的目标很明确:把博客补成一个能持续运行的工程,不只是一个能展示的页面。


第一件事:把首页、归档页、文章页接进预渲染链路

原站点是纯 CSR。浏览器打开当然没问题,但对于搜索引擎、抓取器或者分享机器人来说,它们拿到的往往只是一个空壳。

这次做法是:

  1. 先正常执行 vite build
  2. 启一个只服务 dist 的本地静态服务。
  3. 用 Playwright 逐个打开 //archive/news/post/:slug
  4. 等页面主体真正渲染完成后,把最终 HTML 写回对应路由目录。

预渲染流程图

这个过程中真正踩到的坑有两个。

1. 不能用 networkidle 当成“页面好了”

在实际页面里,第三方请求、懒加载和异步模块都有可能让 networkidle 等不到,或者等得太久。

更稳的做法是:

  • 先等 domcontentloaded
  • 再等页面主体元素出现
  • 同时避开还在显示的骨架屏

这比单纯看网络状态更贴近“用户已经能看到内容”。

2. /news 这种无扩展名路由,必须统一回到壳文件

之前本地预渲染 /news 超时,不是因为 News 页面本身坏了,而是静态服务把无扩展名路径错误地解析到了已有输出目录,导致拿到的不是正确的 SPA 壳文件。

修复后,像 /news/archive/post/... 这样的前端路由,都会先回到主壳,再让前端自己渲染路由内容。这样预渲染才有正确的起点。


第二件事:把 /news 从“看起来在线”补成“仓库和线上一致”

线上 https://techloop.online/news 当时已经有 7 篇 news,但本地仓库只有 4 篇。
这意味着本地一旦部署,很可能把线上已经存在的日报重新覆盖掉。

问题本质不是页面错了,而是内容源断开了同步。

news 同步与服务器回写流程图

后来确认缺的是这 3 篇:

  • ai-dev-daily-2026-05-22
  • ai-dev-daily-2026-05-26
  • ai-dev-daily-2026-05-27

处理方式分两步:

1. 先把缺失日报补回本地仓库

这一步做完之后,本地 /news 和线上终于对齐,构建出来的 news 列表也恢复一致。

2. 再把服务器的“生成日报”改成“生成后自动回写仓库”

这一步补了几个关键配置:

  • AUTO_PUSH_DAILY_NEWS=true
  • Git 远端与分支
  • Git 作者信息
  • 服务器工作目录改成真实 Git worktree

这样以后服务器生成了新的日报,本地只要 git pull 就能拿到,不会再出现“线上有,本地没有,一部署就冲掉”的状态。


第三件事:把部署链路里两个真正会卡死发布的问题补掉

问题一:服务器环境变量里带空格的作者名没有加引号

当时服务器的配置里写的是:

GIT_AUTHOR_NAME=TechLoop Bot

Shell 在读取时会把 Bot 当成一个命令去执行,于是直接报:

Bot: command not found

正确写法应该是:

GIT_AUTHOR_NAME="TechLoop Bot"

这是一个很小,但会直接让发布失败的部署问题。

问题二:服务器没有 Chrome/Chromium,预渲染会整段失败

本地之所以能预渲染,是因为本机本来就有可用浏览器。
但服务器没有安装 Chrome/Chromium,prerender.mjs 在服务器上执行时会直接退出。

这次我做了两层处理:

  • 补了 Linux 常见浏览器路径探测
  • 如果服务器没有浏览器,就优雅跳过预渲染,不让整个发布失败

这意味着当前线上部署已经能稳定完成。
如果后面你想让服务器也真正产出静态化的 HTML,再补装 Chromium 即可。


顺手做掉的三个收口项

除了主线问题,这次还顺手把几个容易拖慢体验的小问题一起收了。

1. 补了无障碍基线

  • skip to content
  • 更清晰的 focus-visible
  • 导航当前状态标识
  • 菜单支持 Esc 关闭

这些改动不显眼,但能显著改善键盘导航和基础可访问性。

2. 整理了移动端主题入口

原来移动端切换主题的入口藏得太深。现在 Header 上有独立按钮,不用先展开菜单再操作。

3. 补齐了 3 篇日报的本地封面图

这 3 篇补回来的 news 原本没有 coverImage,列表页会出现大片空白占位。
这次直接补成了本地 SVG 封面,避免继续依赖空白占位或外链资源。


这次最大的收获,不是“修好一个 bug”

这次更像是把博客从“页面能展示”推进到“工程能长期维护”。

我更在意的是下面这几个变化:

  • 构建结果开始对搜索抓取更友好。
  • news 不再是线上和本地两套状态。
  • 服务器新增内容后,会自动回写仓库。
  • 部署链路不会再因为一个空格或一个缺失浏览器直接中断。

这些工作不会像改首页那样立刻带来视觉冲击,但它们决定了这个博客以后是不是能稳定迭代。


下一步还值得继续做什么

如果继续往下走,我会建议优先做三件事:

  1. 在服务器安装 Chromium,让线上也真正产出预渲染页面。
  2. 给文章页补阅读进度、TOC 激活联动和返回顶部。
  3. 继续把图片资源整理成更系统的生成规范,比如统一封面尺寸、命名和压缩流程。

如果一个博客准备长期写下去,内容、构建、同步和部署最好在同一个工程闭环里。
这次做的,就是把这个闭环先搭起来。


See also