在 Cloudflare Pages 上 静态优先
搭一个多语言 AI 婚礼视频站
功能上服务三类客户(新人、家庭、远距亲友),技术上把 80+ HTML 页面推到 CDN 边缘,在 Workers 里加 8 步中间件管线做 i18n 注入、CSP nonce 生成与 SEO 头规范化,再把表单与死信队列写到 KV。下面两张图先讲清楚用户能看到什么,再讲清楚代码是怎么组织的。
访客能看到 五大模块
从访客视角看,这个站分五块:三组服务(个性化、视频增强、离别补偿)是核心转化点;博客 + 资源中心是 SEO 引流的主入口;定价 + 订购 + 成功页是收口;信任与合规层散落全站。下方是按转化路径梳理的页面树。
技术上分 六层 + 一条工具链
所有静态 HTML/CSS/JS/图片推到 Cloudflare CDN 边缘,五个 Workers 函数跑中间件和 API,KV 承担限流和死信队列。构建/部署工具全在根目录 Python 脚本和 internal-tools/ PowerShell 里,不进 deploy 包。
客户端静态层 · Static Client
边缘计算层 · Cloudflare Workers (functions/)
数据层 · Storage Bindings
第三方集成 · Third-Party
路由 & 安全配置层 · Edge Config
构建 & 运维工具链 · Tooling
根 .py + scripts/*.py 走 .assetsignore 排除,不进 deploy 包。internal-tools/ 同上(额外 _redirects 兜底 404!)
一个 /zh/services/personalization/pet-wedding/ 请求的 8 步
挑了一个最常见的路径:中文用户访问宠物婚礼服务页。走完这条管线,浏览器拿到的 HTML 已经是完全中文化的、nonce 已注入、CSP 头已下发、hreflang 全套齐全的最终态。
-
STEP 01 · EDGE RECEIVE
Cloudflare 边缘节点接收请求
DNS 解析到最近的 PoP,命中 Cache-Control: max-age=60(HTML) 或直接 miss。CDN 缓存未命中,把请求转给同节点的 Workers runtime。
-
STEP 02 · _MIDDLEWARE.JS LANG DETECT
中间件识别 /zh/ 前缀,加载 zh 语言包
从 URL 路径第一段
/zh/拿到目标语言 zh,跳过 Accept-Language 自动重定向(已经在 /zh/ 下)。检查wa_lang_usercookie 确认是否用户主动切过。 -
STEP 03 · STATIC FILE FETCH
从静态资源拉 /services/personalization/pet-wedding/index.html
中间件内部 fetch 同源 URL(去掉 /zh/ 前缀),拿到英文 HTML 模板。KV 零交互——这一步纯粹是边缘缓存里的静态文件。
-
STEP 04 · I18N INJECTION
加载 i18n/zh.json,把 data-i18n key 替换为译文
正则扫描所有
data-i18n="architecture.key_placeholder.00000001"元素,用 zh.json 里的值覆盖 innerText。Googlebot 在这一步就拿到完整中文 HTML,不是英文源 + 客户端 JS 翻。 -
STEP 05 · HEAD NORMALIZATION
改 <html lang="zh">、canonical、hreflang、og:locale、JSON-LD
五个 head 元素逐个重写。canonical 指向 /zh/services/.../,hreflang 列出全部 10 种语言,og:locale 改 zh_CN,JSON-LD 的 inLanguage 改 zh。这一步让每种语言都拥有独立可索引的 URL。
-
STEP 06 · CSP NONCE GEN
用 crypto.getRandomValues 生成 16 字节 nonce
每个请求独立 nonce,base64 编码后 22 字符。把 HTML 里所有
gJeR5w0BWoD4l1+HaZBkBg==占位符 + 所有 <script> 标签(没 nonce 的)都注入这个 nonce。 -
STEP 07 · CSP HEADER SET
把 nonce 写入 Content-Security-Policy 响应头
script-src 加
'nonce-XXX' 'strict-dynamic',允许 GA4/AdSense/Cloudflare Insights 加载。HTML 里的 nonce 和 CSP 头里的 nonce 必须严格一致——记忆里 2026-07-29 的故障就源于保存了 post-middleware HTML 导致硬编码。 -
STEP 08 · BROWSER RENDER
200 OK, 浏览器解析 + 预连接 + 加载 nonce'd JS
preconnect 已经在 _headers 里 Link: 预热 GA/AdSense/Insights。main.min.js、i18n.js、langs.js 因为带了合法 nonce,被 strict-dynamic 放行。语言切换器、cookie consent banner、GA4 事件全部正常加载——一切就位。
找东西 来这里翻
如果你要改某个功能,先看它落在哪个组、对应哪些文件、有什么坑。memory 里那些反复踩的坑都收在"已踩过的坑"里。
- URL 重写
- _redirects — 11 类规则;.html 308→目录 / 短链 308→完整路径 / /xx/* 200 内部 rewrite / 旧 blog 301 / 内部文件 404!
- 语言切换
- _middleware.js 边缘注入 + js/i18n.js 客户端切换器 + js/langs.js 语言元数据 + i18n/{lang}.json 翻译源
- hreflang / canonical
- 静态写入每页 head;中间件按访问语言重写 canonical + og:locale + JSON-LD inLanguage
- 图片 pipeline
- 源图 → generate-responsive.py(WebP/PNG 多断点)→ generate-avif.py(AVIF)→ inject-srcset.py + inject-avif-source.py(写 HTML)→ 手动 fix Windows 反斜杠
- 缓存策略
- HTML 60s / blog 300s / sitemap 1h / llms.txt 24h / 图片字体 1y;CSS/JS 用 ?v=N 版本号破缓存
- LCP 优化
- preload LCP hero, AVIF 优先(浏览器自动协商), fonts subset, 关键 CSS 注入, sw.js 离线
- CSP strict-dynamic + nonce
- 中间件每请求生成 nonce, 替换 gJeR5w0BWoD4l1+HaZBkBg== + 给 <script> 自动加 nonce。记住:HTML 源里只能放占位符,不能放真 nonce
- Headers 全家桶
- HSTS 2y · X-Frame SAMEORIGIN · Permissions-Policy 禁用相机/麦克风/支付等 · COOP same-origin · Referrer-Policy strict-origin-when-cross-origin
- 内部文件防泄漏
- .assetsignore 排除 + _redirects `404!` 强兜底,应对 wrangler 4.114 目录排除的 bug
- 分析
- GA4 gtag (lazy 注入) + Cloudflare Web Analytics (beacon, 无 cookie) + AdSense (lazy)
- 死信与重发
- Resend 失败 → /api/dead-letters 写 KV dl:<id> → /admin/leads/ 用 DLQ_TOKEN 看 + 重发
- CSP 违规上报
- __csp_report.js 收 report-uri 端点 + report-to 头,事后扫日志找被 strict-dynamic 误杀的脚本
- 硬编码 CSP nonce
- 曾经保存过 post-middleware HTML 回来,导致 HTML 里的 nonce 不等于响应头 nonce,所有 JS 被 strict-dynamic 静默屏蔽。修复:占位符统一为 gJeR5w0BWoD4l1+HaZBkBg==
- Windows 反斜杠
- Python 用 str(Path(...)) 拼 URL → \images/foo.avif,浏览器不转,全 404。修复:str 后 replace("\\","/") 或用 PosixPath 起点
- 嵌套 <picture> + 相对路径 srcset
- flatten-avatar-wrapper.py 没拆干净 + Windows 反斜杠 + dedupe 正则错配,三连击。特征:浏览器收 nested picture 内的相对 srcset 解析为 /foo.avif(不是 /images/foo.avif)
- Cloudflare Pages dir/index 308 自环
- zone-level URL Normalization 开关会覆盖 _redirects 200 规则,导致 /foo 308 → /foo 自环。修复:关掉 zone 那个开关,改靠 _redirects 处理
- never-upscale 漏配
- srcset 引用了 1280w 但 generate-responsive.py 因为源图只 1280w 跳过了该变体。修复:扩源图到 ≥1920w,手动补 1600/1920w 变体
从用户素材到 AI 成品 的全链路
这站本身不"存储"婚礼视频——用户上传的素材由人工/AI 在线下游处理,成品通过邮件 + CDN 回流。这里画的是数据形态在每个节点的变化,以及哪些字节会落进哪些存储。
三个 zone · 六类节点
把上一节的"逻辑层"翻译成"物理/网络拓扑"。左边是用户和爬虫,中间是 Cloudflare 全球边缘,右边是这台 Windows 工作站 + 第三方 SaaS。所有生产流量都不进工作站,只走 deploy 时一次性把静态包推上去。
三个 端到端 流程
挑了三个最值钱的闭环:客户询价、内容生产 (SEO 引擎)、管理员救火。每个流程都从用户/操作员的动作开始,到系统状态变化或反馈结束。
F-01 新客询价
访客浏览 → 选 SKU → 提交表单 → 邮件回执,平均 4.2 步。
- 访客 浏览 /services/personalization/*/ 选 SKU
- CTA → /order/ 三步表单(套餐 / 资料 / 确认)
- POST /api/quote (Workers 接收)
- 校验 字段完整性 + 邮箱格式 + 反垃圾词
- 限流 KV rl:<ip> 5/min/IP, 超 → 429
- Resend 发客户确认 + 副本给 admin
- 成功 302 → /success/ (noindex) + GA4 event
- 失败 Resend 报错 → 写 KV dl:<id>, 返 200 让客户看到 /success/ (回执异步补发)
F-02 博客 SEO 引流
articles/*.md → /blog/<slug>/ → IndexNow 推送 → 搜索收录。
- 作者 在 articles/ 写 .md (含 front matter + body)
- scripts/generate-blog.py 渲染为 /blog/<slug>/index.html
- schema JSON-LD BlogPosting + Article + 10 语言 hreflang 注入
- 内链 qa/dedup_noscript.py 扫 + 修 <noscript> 重复
- 图片 走图片 pipeline (源图 → 多断点 + AVIF)
- 部署 wrangler deploy, 边缘 60s 缓存
- 推送 internal-tools/update-sitemap.ps1 → sitemap-zh.xml + IndexNow
- 收录 Googlebot 1-3 天, Bing 1-7 天, GPTBot 数小时
F-03 死信重发
Resend 失败的 lead 不丢,管理员手动重发,带 token 鉴权。
- 触发 /api/quote 调 Resend 返回非 2xx
- 落 DLQ KV dl:<leadId> 存 lead 全文 + 错误码
- 管理员 访问 /admin/leads/ (页面本身 public)
- 鉴权 输 DLQ_TOKEN → POST /api/dead-letters
- 列表 拿到 dl:* 全表 (按时间倒序)
- 重发 选 lead → POST /api/dead-letters?action=resend
- 校验 二次 Resend, 成功 → 删 KV key
- 失败 保留 KV key + 累加重试次数, 下次再试
F-01 的 5 步时间线
把 F-01 流程压成一张时序图,只看谁打谁、几号返回,极简版不画错码分支。要看完整分支往上看 F-01 卡片。
一个 HTML 页 = head + nav + 8-12 个 section + footer
挑了 5 个典型页面对比:首页(信息密度最高)、服务详情(模板化最强)、博客文章(内容型)、订购(转化型)、404/offline(极简)。每列里 block 的颜色就是它的类型,看图就知道哪些页面结构相似、哪些页面是孤例。
从 articles/foo.md 到 用户看见的中文页面,8 步
一个页面的完整"诞生史":从内容作者写 markdown 开始,经过本地工具链注入,到 wrangler 部署,再到边缘中间件处理,最后被浏览器渲染。中间任意一步挂了,这一篇页面就废。每步都标了"谁负责"和"输入/输出"。
-
WORKSTATION STEP 01 · SOURCE
内容作者写源文件
输入: articles/46-weddingai-video-review.md(含 front matter) · 原图 source-hero-1920.jpg
输出: 一个 .md + 一组原图,放在 repo 本地
负责: 人(你)。这一步完全离线,没碰任何 deploy 资产。 -
WORKSTATION STEP 02 · RENDER
渲染为静态 HTML
输入: .md + front matter
输出: /blog/weddingai-video-review/index.html(含 Schema.org BlogPosting JSON-LD + 10 语言 hreflang + 内嵌 OG meta)
负责: scripts/generate-blog.py(本地 Python) -
WORKSTATION STEP 03 · INJECT
图片 / CSS 注入
输入: 上一步的 HTML + images/source-hero-1920.jpg
输出: 同一 HTML,但 hero img 标签被改写成 <picture> + srcset(1600w/1920w)+ AVIF source,带 width/height,带 preload link
负责: 根目录 *.py(inject-srcset / inject-avif-source / inject-dimensions / inject-critical-css)。 -
WORKSTATION STEP 04 · CHECK
本地 + pre-deploy 校验
输入: 完整部署包 (整站 .html + images/ + js/ + functions/)
输出: 1 个内部 tools pre-deploy-check.ps1 通过信号(无 404 srcset / 无硬编码 nonce / 无相对路径 / 无 /xx/ 死链)
负责: internal-tools/pre-deploy-check.ps1(本地 PowerShell,跑 7 类检查)。 -
WORKSTATION STEP 05 · DEPLOY
wrangler 上传到 Cloudflare
输入: 部署包(已 .assetsignore 过滤,private/qa/scripts/internal-tools/ 全排除)
输出: 边缘节点拿到完整静态资产 + functions/_middleware.js 等 5 个函数
负责: wrangler 4.114(本地执行 deploy 命令,1-3 分钟完成,含 KV 同步)。 -
CLOUDFLARE EDGE STEP 06 · REQUEST
用户浏览器发请求
输入: 浏览器 GET https://weddingai.video/blog/weddingai-video-review/ · Accept-Language: zh-CN
输出: 请求路由到最近的 Cloudflare PoP,Workers runtime 启动 _middleware.js
负责: Cloudflare 全球 Anycast 网络 + Workers 0ms 冷启动。 -
CLOUDFLARE EDGE STEP 07 · TRANSFORM
边缘中间件 8 步管线
输入: 英文 HTML 模板 + 接受语言 zh-CN
输出: 完全中文化的 HTML(<html lang="zh-CN">、hreflang 全套、og:locale、JSON-LD inLanguage、data-i18n 全部翻译) + CSP nonce 已注入 + Content-Security-Policy 头已设置
负责: functions/_middleware.js(详细 8 步见 s3 章节)。 -
BROWSER STEP 08 · RENDER
浏览器加载 + 渲染 + hydrate
输入: 200 OK 中文 HTML + 已注入 nonce 的 <script> 标签 + styles.min.css?v=N + js/main.min.js
输出: 用户看见完整中文页面,首屏 LCP < 1.8s,语言切换器、cookie banner、GA4 全部活起来
负责: Chrome/Safari/Firefox + PWA service worker(sw.js 处理离线 fallback)。