静态站点部署 Cloudflare Pages 实战:构建、域名与缓存排错

想把 Astro、Vite、Next.js 静态站点稳定部署上线?本文详解 Cloudflare Pages 的接入方式选择、构建命令与环境变量配置、自定义域与 DNS 解析、缓存失效机制,并给出一份可对照的排错清单。

静态站点生成器再好用,最后一步”上线”依然是事故高发区。构建命令填错、Node 版本不匹配、输出目录写反、自定义域一直转圈、改了内容线上不变——这些问题几乎每个第一次部署的人都会遇到一遍。

Cloudflare Pages 是目前部署静态站点性价比很高的选择:全球边缘节点、自动 HTTPS、每次提交生成一个预览链接、回滚只是一次点击。这篇文章按真实的部署顺序,把配置要点和排错路径梳理清楚。

先选对接入方式

Pages 有两种接入方式,选错了后面会很别扭。核心区别是谁来执行构建:

接入方式构建在哪里跑优点适合场景
Git 集成Cloudflare 云端零配置,推送即部署,自动生成预览链接绝大多数站点,尤其是纯静态站
直接上传(Wrangler)你自己的机器或 CI构建环境完全可控,可用任意私有依赖构建脚本复杂、依赖私有包、需要在 CI 内统一发布

默认选 Git 集成。 它能白送两个很值钱的能力:每个分支和 PR 一个独立预览链接,以及生产环境一键回滚。只有当你的构建流程确实跑不进云端环境(例如需要内网依赖、构建耗时要几十分钟),才考虑用 Wrangler 本地推送产物。

Wrangler 上传的命令也很简单:

# 安装并登录(浏览器完成授权)
npm i -D wrangler@latest
npx wrangler login

# 本地先构建,再上传产物目录
npm run build
npx wrangler pages deploy dist --project-name=my-site

# 只想先看看效果,部署到预览环境
npx wrangler pages deploy dist --project-name=my-site --branch=preview

把构建配置锁死

构建配置里只有三个字段是必填的,但每个字段填错都会导致失败:

框架构建命令输出目录注意点
Astronpm run builddist纯静态无需适配器;用了 SSR 适配器则不适用 Pages 静态模式
Vitenpm run builddist要注意 base 配置,部署在子路径时必须同步
Next.jsnpm run build && npm run exportout需在配置中开启静态导出
Hugohugo --minifypublic云端镜像需指定 Hugo 版本,扩展版需手动声明

除了这三个字段,还有两件事值得提前做:

第一,锁定 Node 版本。 云端默认的 Node 版本不一定和本地一致,版本漂移会带来”本地能跑线上报错”的经典问题。用环境变量显式指定:

NODE_VERSION = 22.11.0

如果不想改平台配置,也可以在 package.json 里加 engines 字段作为约束,双保险。

第二,依赖安装命令的选择。 有 package-lock.json 时用 npm ci,它会严格按锁文件安装且速度更快;没有锁文件才用 npm install。没有锁文件的项目在 CI 里安装是不确定性的主要来源,建议本地生成后提交到仓库。

{
  "scripts": {
    "build": "astro build"
  },
  "engines": {
    "node": ">=22"
  }
}

环境变量与密钥的正确用法

构建期需要的变量(如 API Key、站点 URL)在 Pages 的 Settings → Environment variables 里配置,并且可以分别为生产环境和预览环境设置不同的值。

关键约定有两条:

  1. 框架对变量名前缀有要求。 例如 Astro 的公开变量需要 PUBLIC_ 前缀才能被打包进客户端代码,Vite 需要 VITE_ 前缀。直接写 API_URL 在客户端读不到,这是最常见的”本地有值线上 undefined”。
  2. 敏感值只能用于构建期。 静态站点没有服务端,任何写进产物的值都会被用户看到。需要私密调用的接口,应该用 Pages Functions 或边缘函数做一层代理,而不是把密钥打进前端 bundle。
# 本地用 .env 模拟,注意 .env 不要提交
PUBLIC_SITE_URL=https://example.com
API_TOKEN=xxxx          # 仅构建期使用,不要暴露给客户端

自定义域与 DNS 解析

在 Custom domains 里添加域名后,推荐做法是把域名的 NS 记录托管到 Cloudflare,这样证书会自动签发和续期,通常几分钟内生效。

如果 DNS 保留在原服务商,也可以手动添加 CNAME 记录指向你的 *.pages.dev 域名,再在面板中选择高级证书手动验证。代价是证书续期需要自己盯,适合确实不方便换 NS 的情况。

几个实践中容易踩的点:

  • 根域(example.com)用 CNAME 存在争议,托管到 Cloudflare 后会自动使用扁平化处理,更省心。
  • 先加 www 再决定跳转方向,把其中一个设为规范版本,另一个做 301 跳转,避免两个域名同时被 indexing 造成权重分散。
  • 验证失败先看 DNS 传播,用 dig 确认记录已生效再点重试,反复点击重试不会加速。
# 确认解析是否已生效
dig example.com +short
dig www.example.com CNAME +short

# 确认证书与响应头
curl -I https://example.com

缓存机制:为什么改了看不到

这是 Pages 上最高频的困惑。要理解一件事:每次部署的产物文件名带哈希,静态资源基本不用担心缓存问题;真正容易出问题的是 HTML 文档本身。

处理顺序建议如下:

  • 修改后先在 Deployments 列表确认这次构建状态是成功,而不是看首页有没有变。
  • 确认最新版本上标记的是”生产”,Pages 会保留部署历史,可以点选任意一条回滚。
  • 如果确认已发布仍看到旧内容,再考虑缓存。用 curl -I 看响应头里的 cf-cache-status,HIT 表示命中边缘缓存。
  • 需要立即使全站缓存失效时,用 API 触发一次清除比在面板里等更可控。
curl -sS -X POST \
  "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/purge_cache" \
  -H "Authorization: Bearer $CF_API_TOKEN" \
  -H "Content-Type: application/json" \
  --data '{"purge_everything":true}'

全站清除是”核弹选项”,会让整站回源、短期增加源站压力。日常更推荐用带哈希的文件名 + 合理的 HTML 缓存头,让新版本自动生效,而不是依赖手动清除。

一份可对照的排错清单

现象最可能原因处理方式
构建报「找不到输出目录」输出目录填错本地构建一次,确认产物实际目录名
本地能构建,线上报依赖错误缺锁文件或 Node 版本不一致提交 lock 文件,显式设置 NODE_VERSION
页面能开,样式全丢部署在子路径但 base/site 未同步在框架配置中填写完整站点 URL
刷新子页面 404未配置 SPA 回退或缺少 404 页面加 _redirects 或提供 404.html
自定义域一直”验证中”DNS 未传播或记录类型错误dig 确认后重试,必要时等待传播
环境变量读不到缺少框架要求的前缀Astro 用 PUBLIC_、Vite 用 VITE_

其中”刷新子页面 404”值得单独说明:静态托管默认按文件路径查找,访问 /posts/hello 会去找同名文件。单页应用需要一条回退规则:

# public/_redirects
/*    /index.html   200

上线前的检查清单

  • 本地执行过一次干净构建,产物目录确认无误
  • 依赖锁文件已提交,Node 版本已显式指定
  • 环境变量按框架前缀命名,敏感值未进入客户端产物
  • 自定义域已生效,且 http:// 与 www 均 301 到规范版本
  • robots.txt 与 sitemap.xml 可访问
  • 404 页面存在,且访问不存在的路径会返回 404 状态码而非 200
  • 确认过一次回滚操作,知道出问题时点哪里

最后一条最容易被跳过。部署系统的价值不在于”能发布”,而在于”发错了能三秒内退回去”。 上线前花一分钟点开部署历史,确认回滚按钮在哪,比事后翻文档有用得多。

常见问题

免费额度会不会突然不够用?

个人内容站通常远低于免费额度上限。但具体配额(每月构建次数、带宽、并发构建数、可绑定的自定义域数量)会随官方策略调整,请以 Cloudflare 官网定价页为准。如果站点开始有稳定流量,建议提前确认一下当前的限额说明,避免临时被限流。

部署成功但访问是 404,最可能的原因是什么?

绝大多数情况是构建输出目录填错了。Astro 与 Vite 默认是 dist,Next.js 静态导出是 out,Hugo 是 public。平台报「找不到输出目录」时,先在本地跑一次 npm run build,用 ls 看产物实际落在哪个文件夹,再回填配置重新部署即可。

自定义域接入后一定要把 DNS 托管到 Cloudflare 吗?

不一定。托管 NS 到 Cloudflare 时证书会自动签发与续期,配置最省事,是默认推荐路径。DNS 留在原服务商也可以,需手动添加 CNAME 并用高级证书手动验证,代价是证书续期要自己关注。两种方式在访问速度上没有本质差异。

改了内容但线上没更新,应该怎么排查?

按顺序查四件事:构建日志是否真的成功、是否部署到了预期环境、最新那条部署是否标记为生产、浏览器与 CDN 缓存是否已失效。多数”没更新”其实是新部署还停在预览环境没有提升为生产版本;如果确实已发布,再用 curl -I 看 cf-cache-status 是否为 HIT,决定是否需要清除缓存。

#Cloudflare Pages#静态站点部署#CI/CD#性能优化

常见问题

Cloudflare Pages 免费版够用吗?
对大多数个人博客和内容站来说,免费版的构建次数、带宽与站点数量通常已经足够。具体配额会随官方策略调整,付费方案才包含的预览环境数量、构建并发等指标请以官网定价页为准,不要依据旧文章里的数字做容量规划。
部署成功但访问是 404,最可能的原因是什么?
九成是构建输出目录填错了。Astro 默认输出 dist、Vite 默认 dist、Next.js 静态导出默认 out,填错目录时平台会报「找不到输出目录」。先在本地跑一次构建,确认产物实际落在哪个文件夹,再回填到构建配置里。
自定义域接入后一定要把 DNS 托管到 Cloudflare 吗?
不一定。把域名 NS 托管到 Cloudflare 可以自动完成证书签发与验证,配置最省事;如果 DNS 留在原服务商,也可以在 Pages 的自定义域页面选择高级证书手动验证,只是证书续期需要自己关注。
改了内容但线上没更新,应该怎么排查?
按顺序查四件事:构建日志是否真的成功、是否部署到了预期环境、是否为最新那条部署点了「设为生产」、浏览器与 CDN 缓存是否已失效。多数「没更新」其实是新部署还停在预览环境,没有提升为生产版本。