这是「搭建这个博客」系列的第 3 篇。前两篇是 1 从零搭一个博客 和 2 给静态博客接一个能登录的后台。
前两篇讲的是”怎么做对的”。这篇讲”怎么错的”——而且我保证,这一篇比前两篇有用。
顺便说一句:整个博客是我和 AI 协作做的(也就是现在说的 vibe coding)。下面这些坑,有一半是它帮我找到的,也有一半是它先带偏、后来才被我拽回来的。最后一节专门聊这件事。
坑 1:一个字符抄错,GitHub 直接给我 404
配 OAuth 的时候,我从 GitHub 页面上把 Client ID 手动敲进了 Cloudflare 的环境变量。
然后点登录:打开 GitHub 授权页 → 404,一只沙漠里的猫告诉我”This is not the web page you are looking for”。
最要命的是这个错的表现形式,它几乎把所有常规排查方向都指向了错误的地方:
- 如果我没登录 GitHub,打开那个授权地址,它会正常跳转到登录页——看起来”授权页是通的”!
- 只有登录之后,GitHub 才真正处理这个授权请求,这时才发现找不到这个应用 → 404
于是接下来的一个多小时,我依次怀疑并排查了:网络被墙、浏览器缓存、油猴脚本、代理软件、DNS、hosts……一个都不是。
真正的原因是:我把第 5 位的小写字母 l 敲成了数字 1,还漏掉了一个字符——19 位,而正确的是 20 位。
教训:
Client ID/Client Secret这类东西,永远用复制按钮,不要手打。它们的字符集里全是l / 1 / I、O / 0这种长得一模一样的东西。- 自检方法:这类 client ID 通常是 20 位。长度不对,基本就是抄错了。
- 排查顺序:先怀疑自己刚输入的东西,再怀疑环境。我是反着来的,所以白花了一个多小时。
坑 2:无刷新跳转,把页面卡在了半截
Fuwari 用 Swup 做无刷新页面切换——点站内链接时不整页刷新,只把 <main> 里的内容换掉。体验确实好,但有个前提:目标页面得有 <main> 容器。
而后台页面(CMS)是独立的单页应用,压根没有 <main>。
于是点导航里指向后台的链接时,浏览器控制台里出现:
[swup] Container missing in incoming document: main[swup] Container mismatch, abortingSwup 一看容器对不上 → 把整个跳转中止了 → 页面就停在”切了一半”的状态,导航栏上打开的浮层也跟着挂在那儿不动。
这也是我一开始判断错的地方:我以为是”面板关闭逻辑写错了”,改了好几轮都没用。因为问题根本不在关闭逻辑,而在”跳转失败了”。
解决:给指向后台的链接加一个 data-no-swup 属性(这是 Swup 的忽略标记),让它老老实实整页加载。后台本来就是独立应用,整页加载才是对的。
坑 3:bfcache 让”已经关掉”的面板又活了
我做了个导航栏浮层面板,逻辑是”打开时关掉别人,离开页面时关掉自己”。听起来没毛病。
但浏览器前进后退的时候出问题了:
你点了链接离开 → pagehide 触发 → 代码把状态设成「关闭」 ↓ 但框架是异步更新 DOM 的 浏览器立刻把页面冻结进 bfcache (DOM 上还是「打开」的样子) ↓ 你按了后退,页面从缓存里被恢复 状态已经是「关闭」→ 框架认为没变化 → 永远不再渲染 ↓ 面板就永久卡在打开的样子,只有刷新能救关键点在于:bfcache 冻结页面时,JS 状态和 DOM 会一起被原样保存,而且 pagehide 之后留给你的时间非常短。你改了状态,但框架还没把 DOM 更新出来,页面就已经被冻住了。
解决:不要依赖框架重渲染——直接操作 DOM 类名(classList.add(...)),并且在 pageshow 时再对齐一次。
普遍启示:任何”离开页面时改状态”的逻辑,都要考虑 bfcache。这不是框架的 bug,是浏览器的正常行为。
坑 4:改了环境变量,必须重新部署
这个纯属不知道就会踩:Cloudflare 不会把新填的环境变量自动应用到已经跑着的 Functions 上。
现象是:变量明明填对了,接口却一直报”未配置”。去部署页面点一次”重新部署”就好了。
坑 5:“代码没生效”十有八九是浏览器在跑旧代码
调试期间反复出现”我明明改了,怎么还是老样子”。后来发现是两类原因:
- 开发服务器的热更新(HMR)能更新模板和状态,但不重新绑定已经挂载的组件的事件逻辑。你改了事件处理,页面不整页刷新就还是旧的。
- 浏览器前进后退会恢复缓存页面,看起来就像”我刚改的东西消失了”。
解决:我在组件里加了一行版本标记——window.__adminMenuV = "v6"。不确定当前页面跑的是哪一版,控制台敲一下就知道。就这一行,省了后面大量的”到底生效了没”的扯皮。
坑 6:我自己的测试,把我自己骗了
这条最值得说,因为它跟代码无关,跟”怎么确认问题真的解决了”有关。
修完 bfcache 那个 bug 之后,我写了个自动化测试来验证。测试通过了。但线上问题还在。
原因是我用脚本合成的 pagehide / pageshow 事件去模拟”前进后退”。而合成事件根本不会触发真正的页面冻结——于是 DOM 更新有充足的时间跑完,那个 bug 自然复现不出来。测试给了我一个绿色的假象。
后来我把测试改成两条硬要求:
- 用真实的导航:
location.href真的跳走,再history.back()真的回来 - 断言”这次确实走了 bfcache”:在
pageshow里记录event.persisted,如果它没变成true,这次测试就不算数
改完之后测试才真正有价值——它才真的在测那条会出问题的路径。
教训:测试通过 ≠ 问题解决,尤其是当测试自己构造了运行环境的时候。多问一句:“我这么测,有没有可能绕过了真实路径?“
关于 vibe coding:它改变了什么,又要求什么
这个博客是我和 AI 协作做的。整个过程里我几乎没手写代码,但每一步都在做判断。
它确实很能打
- 一个下午搞定整个站点的汉化、配置、若干交互组件
- 几十行写出并部署了 OAuth 代理,还顺手做了 CSRF、白名单、最小权限
- 排查问题时能快速给出可验证的假设(例如”直连返回 302、走代理返回 404,所以问题在本机代理”——这个假设完全正确,一下省了我半小时)
放在以前,这些事情的时间成本大概是这里的几倍。
但它有硬性要求
1. 验证权必须留在人手里。 它会非常自信地给结论,包括错的结论。这一轮里它先后把问题归因于”缓存""浏览器扩展""本机代理”,全都不是。关键结论自己复现一遍,这是底线。
2. 凭据永远不进仓库、不进聊天。 Client Secret、访问令牌这类东西只能待在部署平台的环境变量里。我中途把 secret 截图发出去过,事后只能作废重发。能不发就不发。
3. 小步提交,保持可回滚。 每次改动独立成一次提交,写清”改了什么、为什么”。出问题时能一眼定位到是哪一步引入的——这一条救过我不止一次。
4. 让它承认”我不确定”。 比”答错了”更危险的是”答得很顺但你没法验证”。所以我会强制它给依据:日志、状态码、测试结果。没有证据的结论,一律当假设。
5. 关键代码你得看得懂。 不用逐行懂,但安全相关的必须懂:谁能拿到 token、白名单在哪校验、secret 存在哪。看不懂的地方,就不该让它上线。
6. 文档要写”为什么”。
我把踩过的坑和排查方法都写进仓库的 docs/ 了。三个月后的我,或者下一个接手的人,能少走一遍。
7. 接受”它会自信地犯错”,并据此设计流程。 所以要有版本标记、要有可复现的测试、要有能回滚的提交历史。这些不是仪式感,是保险。
我的结论
vibe coding 没有让”懂技术”变得不重要。恰恰相反,它把人的角色从”实现者”推到了”审查者”——而审查需要的判断力一点不比写代码少:
- 看得出方案哪里不对
- 读得懂报错在说什么
- 问得出”这个测试有没有绕过真实路径”
- 守得住”凭据不进仓库”这种底线
它能极大压缩”实现”的时间,但压缩不了”判断”的责任。
最后
这三篇写下来,其实我想说的就一句:搭博客这件事,现在真的不难了。难的是那些”看起来好了其实没好”的时刻——而对付它们,靠的不是更聪明的工具,是更笨的验证。
如果你也在搭,希望这几篇能帮你少花一个多小时。
系列导航:1 从零搭一个博客 · 2 给静态博客接一个能登录的后台 · 3 卡了我半天的那些坑