一篇 Markdown 文章到读者浏览器之间,至少有三次转换:Astro 读取内容并生成页面,Wrangler 上传指定构建目录,Cloudflare 根据请求路径交付资源。部署成功只证明其中一段完成,不能证明新文章存在、私有草稿没有泄漏,或者自定义域名已经指向这份产物。
本文用 Tony’s Notes 的静态博客结构说明这条链路。目标是能解释每个文件在哪里产生、由谁公开,以及出错时应该检查哪一层。代码是静态资源方案,不包含登录、数据库查询或 Astro 服务端渲染;后面增加动态能力时,需要重新评估相应边界。
1. 从请求的变化频率决定架构
假设文章一天修改一次,但读者一天访问很多次。如果 HTML 在发布时已经确定,就没有必要让每次请求都重复读取 Markdown、查询内容和执行模板。构建阶段一次生成结果,运行阶段交付文件,能够把内容错误提前暴露,也减少网站可用性对数据库和应用进程的依赖。
这里的「静态」描述生成时点,而不是交互程度。搜索筛选、主题切换或代码复制可以在浏览器执行;需要实时数据的模块可以调用独立 API。但某个组件需要交互,不等于整个页面必须改成 SSR。先问数据何时确定,再问浏览器需要哪些 JavaScript,会比先选择一套运行时架构更清楚。
反例是私人账户页:即使页面外观长期不变,内容也取决于当前身份。把它预生成成公共文件不能获得访问控制。表单写入、支付和数据库事务同样需要可信服务端边界。缓存公开文章的经验不能直接用于这些请求,更不能把浏览器里隐藏链接当成权限检查。
2. 用目录表达公开与私有的边界
本项目的路径分工可以直接检查:
src/content/writing/ 参与 Astro 内容集合的文章
src/pages/ 页面与路由
public/ 按原样进入公开产物的文件
.drafts/ 本机私有稿件,不参与公开构建
dist/ 当前构建结果,交给 Wrangler
最容易误用的是 public/:它不是「方便模板读取的资料目录」,而是公开资源入口。把草稿索引、内部附件或诊断结果放进去,就算首页没有链接,猜到路径的人仍可能获取。相同道理,Git 忽略一个文件只控制版本跟踪,不会阻止构建脚本复制它。
私有草稿因此放在项目根目录的 .drafts/,不由内容集合加载,也不位于 public/ 内。发布流程只把已选中的文章复制到公开内容目录。这个选择是本项目的内容工作流,不是 Astro 自动提供的私人后台;直接把整个草稿目录打包部署,会绕过所有状态标签。
审计要沿产物检查,而不只看源文件位置。构建完成后确认 dist/ 没有私有文件、未发布标题和草稿路由。代码生成器、全文搜索索引和 RSS 都可能形成另一条出口,新增这类功能时,必须重新检查它们实际读取的集合。
3. 把构建配置与交付配置分开
Astro 的 site 代表正式网站地址,站点地图等集成会用它生成链接。当前项目采用静态输出,不需要为了使用 Workers 就添加 SSR adapter。先保留最少配置,能减少运行时概念与静态资源概念混用。
import { defineConfig } from 'astro/config';
import sitemap from '@astrojs/sitemap';
export default defineConfig({
site: 'https://taotao.dev',
integrations: [sitemap()],
});
site 不会自动给每个页面注入完整 canonical,也不会替你建立 DNS 或把 www 重定向到根域。页面布局、RSS 实现、站点地图和域名配置有各自消费者,需要逐项核对。只看到配置中有正式域名,不能据此推断所有生成链接都正确。
Wrangler 负责资源上传与 Workers 部署:
{
"name": "taotao-dev",
"compatibility_date": "2026-10-11",
"assets": {
"directory": "./dist",
"not_found_handling": "404-page"
}
}
这是一份节选,实际项目还包含自定义域名配置。directory 指定上传内容,不指定 Astro 的输出方式;两边必须一致。没有 Worker 脚本入口的纯静态部署由平台交付资源,不需要再写一个无业务意义的转发函数。兼容日期也不是软件版本或发布日期,改变它时仍应查对应行为说明。
4. 一次构建应该得到可解释的产物
先在与项目兼容的 Node 环境安装依赖。仓库中已有锁文件时,自动化环境通常使用 npm ci,让解析结果跟随锁文件;如果依赖清单与锁文件不一致,先解决不一致,不要靠重新解析悄悄得到另一组版本。
npm ci
npm run check
npm test
npm run build
四个步骤回答不同问题:安装解析依赖,类型检查发现代码问题,测试验证约定,构建生成真正交付的资源。测试通过不等于当前构建产物正确,构建成功也不等于文章事实正确。内容 schema 能发现缺字段,却不能确认代码示例的逻辑或引用是否支持结论。
构建完成后查看目标文章的 HTML、RSS 和 sitemap,再打开本地预览。检查标题、日期、链接和代码块是否符合预期,而不是只数页面数量。新增一篇文章时,如果产物页数没有变化,需要区分是草稿过滤、slug 冲突、文件没被 loader 匹配,还是正在查看上一次构建。
发布工作区最好与正在写作的工作区分开。若构建期间有人继续改文件,上传可能对应既不是旧提交也不是新提交的混合状态。手动发布至少在构建前后核对工作区,自动化则记录明确的提交与产物关系,避免把版本号当成唯一追溯依据。
从同一篇文章追踪构建输入
假设修改正文后本地首页出现新摘要,生成的文章 HTML 却仍是旧内容。不要先调整 Cloudflare 缓存:此时旧内容已经存在于尚未上传的产物里,网络层不是首个失败边界。先检查运行命令的项目目录、内容集合匹配路径、文章是否仍被过滤,以及读取的是不是本次输出目录。把问题定位到最早不符合预期的转换,能减少无关配置变更。
反过来,如果生成 HTML 正确、Worker 默认地址也正确,而正式域名错误,内容解析阶段已有正例,不需要重新设计 Markdown loader。应核对实际绑定、路由和正式地址返回的正文。每一层都使用同一篇有区分度的文章,避免用首页证明构建正确、用另一篇文章证明部署正确,最终没有任何一条完整链路被验证。
测试输入也要可重复。文章引用外部 API 获取构建数据时,相同源码可能在不同时间生成不同结果;安装时依赖变化或图片转换器升级,也可能改变交付资源。静态输出不等于构建确定性。对需要严格追溯的输入记录版本或保存允许公开的快照,并明确外部失败时是中止构建还是使用旧数据,不能静默把空结果当成最新内容。
5. 404 策略是博客路由契约的一部分
博客文章不存在时,读者应该得到不存在的页面,而不是首页。SPA 回退适合由客户端接管路由的应用,却可能让一个拼错的文章地址返回首页和成功状态。搜索引擎、引用者和自动检查就无法分辨文章是否真的存在。
本项目选择 404-page,并提供 Astro 的 404 页面。行为仍要经过线上请求验证:本地开发服务器、Astro 预览和 Workers Static Assets 的路径处理不是同一实现。尤其要检查带尾斜杠与不带尾斜杠的文章链接,以及不存在的文件扩展名路径。
请求验证还要区分 HTML 页面与独立资源。/writing/astro-workers/ 需要得到文章,资源目录下的脚本需要得到相应文件,不存在的脚本不能返回 HTML 伪装成成功。浏览器可能在解析脚本时才报告语法或 MIME 问题,真正原因却是资源路径走了错误的回退。记录请求 URL、状态、内容类型及正文身份,往往比只看控制台的最后一行错误更接近根因。
查询参数通常不创建另一份 Markdown 文件,但可能参与浏览器或额外缓存规则的处理。检查带参数链接时应保留文章身份,并核对重定向是否意外丢掉参数;对片段标识则要知道它一般由浏览器处理,不随 HTTP 请求发给服务器。修改服务器路由无法直接修复一个页面内部的错误锚点,章节 ID 的生成与链接需要在最终 HTML 里检查。
curl -I https://taotao.dev/writing/astro-workers/
curl -I https://taotao.dev/writing/this-article-does-not-exist/
curl -I https://taotao.dev/.drafts/index.html
第一条应该访问指定文章,后两条应拒绝提供文章或草稿。HEAD 能快速检查状态和头部,却不能确认正文身份;必要时再用 GET 检查标题与内容。返回一个漂亮的错误页面但状态仍为成功,也不满足不存在资源的契约。
6. 自定义域名需要独立验收
Workers 配置中的 custom_domain 表达绑定意图,实际操作还依赖 Cloudflare 账户权限、域名所在 zone 和既有配置。变更前查看目标域名是否承载其他服务,确认新绑定不会替换仍需保留的入口。这里没有必要把每个域名故障都归因于 Astro。
可以把排查分成地址解析、连接与证书、HTTP 响应三层。解析指向哪里,解释请求最初去往哪里;TLS 是否成功,解释连接是否建立;返回的文章内容,解释最终是哪份应用产物。三层信息混在一张浏览器错误截图里,容易导致错误层面的反复调整。
如果同时提供根域与 www,还要定义规范地址。两者都能访问不代表其中一个会自动跳转,也不代表 canonical 已统一。选择一个对外地址,然后在适用的平台规则或应用路由中实施跳转,并检查状态、目标和查询参数的处理。避免根域跳向 www、www 又跳回根域的循环。
DNS 与证书的变化可能不是每个网络立即观察到同一状态。记录检查时间和入口,从至少一条实际用户访问路径获取证据,不能仅凭控制台显示「已绑定」宣布网页已经可用。测试时也不要将忽略证书错误的结果当成正式成功。
7. 缓存策略应与文件生命周期一致
带内容指纹的构建资源可以长期缓存,因为内容改变会产生新地址。HTML 地址通常保持不变,需要考虑更新时读者如何获得新版本。不能为了减少请求,给所有路径无差别设置很长缓存,再期待每次部署后文章立即更新。
平台的默认缓存、浏览器缓存与额外规则应分别检查。排查旧内容时,先确认部署版本及请求路径,再比较响应头和正文。一次强制刷新变新只说明这条访问路径受到缓存影响,并没有证明其他读者已经切换。也不要为了临时修复某篇文章而清理所有无关域名的缓存。
对本站来说,文章正文与列表、RSS、sitemap 是同一发布的一组消费者。正文已经更新但订阅源还是旧版本,读者仍可能错过内容。检查这些入口的更新关系,比只测首页更接近真正发布结果;第三方 RSS 阅读器的缓存则不由网站完全控制,说明结论时需要保留这一边界。
更新 HTML 时不要让资源引用失去对象
如果 HTML 引用了带指纹的脚本,两个文件在逻辑上是一组结果。旧页面仍在浏览器中存活时,可能继续请求旧指纹地址;新页面则应引用新资源。新部署怎样提供历史资源、某个缓存层是否仍交付旧 HTML,都需要按平台实际行为验证,不能假设文件名带指纹就自动解决整个版本切换协议。
静态文章通常可以少用客户端脚本,从而减少这种配套要求。但样式、图片和字体仍有同类依赖:页面返回正确标题,图片路径已经失效,阅读体验依旧不完整。对发布后的代表页面获取实际引用列表,检查必要资源的状态与内容类型,不把文章 GET 成功当作所有资源可达。外部图片服务属于另一条可用性路径,本站部署无法替它承诺持续响应。
缓存诊断要先写明观察条件:哪个 URL、是否登录、浏览器是否已有缓存、是否经过额外代理、观察到哪个正文标识。无差别加随机参数或清空全部缓存,可能暂时得到新内容,却掩盖真实读者的路径。先保存旧响应证据,再在受控请求中改变一个条件,才能判断差异来自浏览器、平台交付还是应用版本,而不是把偶然变新写成根因已经找到。
8. 动态能力出现后再引入运行时
以后增加联系表单或需要服务端权限的接口时,可以评估独立 Worker API,或为适当路由引入 Astro 的服务端能力。选项取决于数据、认证、部署与维护边界,不应为了看起来统一,就让所有已经适合静态交付的文章重新经过数据库。
运行时秘密必须由服务端环境管理,不写进 public/,也不写进会被浏览器读取的构建常量。某个环境变量只在本机 .env 中存在,不表示引用它的结果不会进入客户端包。审查最终产物中是否出现秘密,比仅检查文件有没有被 Git 忽略更直接。
动态接口还带来错误传播、日志、限额和访问控制。它们不能由静态资源 404 策略代替。新增一个接口时,保留静态文章的原验收,同时补充接口自己的认证、输入和失败测试,让架构扩展有清楚的证据,而不是把原有成功结论无限外推。
小范围动态化也要明确数据流
例如给静态文章增加收藏按钮,浏览器发送的是用户动作,API 保存的是当前身份下的收藏关系,页面正文仍由静态资源交付。要决定登录凭证如何发送、跨源是否需要允许、错误和重复点击如何处理,不能只在按钮组件里保存一个布尔值。浏览器显示“已收藏”与服务器确认写入是不同事实,刷新后是否恢复状态也属于接口协议。
同时检查公开与个性化响应是否进入同一种缓存策略。文章可以公共复用,某个用户的收藏列表不应因为 URL 看起来固定便共享给其他人。更不能把私有结果在构建时写进 HTML,让运行时认证成为已经泄漏之后的补救。把公开正文、身份查询和写操作列为不同请求,分别设计缓存与权限,比让所有数据经过同一个模糊的“页面接口”更容易验证。
动态接口变更还需要独立失败预算。API 暂不可用时,文章是否仍能读、按钮是否明确失败、是否保留稍后可安全重试的意图,都是产品选择。静态站的优势可以保留下来:非关键交互失败不必让正文消失。对关键写入则不能用乐观文案隐藏未知结果,架构拆分的价值是故障边界清楚,而不仅是文件分成两个目录。
预览产物与正式产物不要共享模糊的发布资格
同一个 dist 目录先用于公开预览,随后上传正式 Worker,意味着预览也是一次真实内容交付。noindex 可以表达索引意图,却不让文章变成私密。预览若包含未批准内容,需要真正受控的访问入口;正式上传则只接受已审核公开集合。环境名称叫 preview,并不能替代这个边界。
验收用一篇允许公开的合成正文和一个私人哨兵,分别核对两个环境允许的范围。需要跨环境复用产物时,先证明其内容资格一致,再检查域名和响应策略;不要因为目录名相同就把测试版本认作正式版本。
9. 用一条文章完成端到端验收
验收记录先填一篇目标文章的源路径、产物路径和正式地址,再逐项附上检查结果。源文件属于公开集合,产物包含指定标题,正式 GET 返回相同正文,RSS 与 sitemap 使用正确地址;某一步失败就停在该边界排查,不让后续成功覆盖前面的缺口。
负例使用一篇未选草稿。本站隔离脚本递归检查产物,拒绝草稿目录,并在受检文本文件里查标题、文件名和详情路由。它没有扫描任意正文片段、压缩文件或所有秘密,不能据此宣称不存在任何泄漏。复制脚本、搜索数据或附件处理改变后,还应按新出口补检查。
最后把提交、Worker、部署版本与观察时间写入同一记录。缺少线上结果时,结论只能到产物已生成或平台已接收;缺少隔离负例时,也不将正例可达解释成私人内容安全。记录的价值是让下一位维护者知道哪一段已有证据、哪一段还要查。