先看出事的那一刻
7 月 28 日,自动驾驶知识站的正式域名刚接通不久。用 curl 分别请求两个地址:后端所在的 Railway 默认域名上,页面标题是新的"E2E REVIEW · 智能驾驶技术站";正式域名上,还是旧的"端到端自动驾驶术语图鉴"。同一个网站,两个版本同时在线。
原因不复杂。前端放在 Cloudflare Pages,由它接住正式域名;后端的镜像里也打包了一整套前端,只要构建产物存在,就自动对外提供。两边各自部署、各自更新,于是分叉了。
这节课要回答的是:一个网站该放在哪、放几处?"顺手多部署一份"会带来什么问题?
装备几个词
为什么要懂
AI 替你做了
接一个托管平台只要点几下授权,AI 还会顺手让后端“也把前端托管起来”。看上去,多一份备份总没坏处。
留给你的
决定每个网站只有一个生产入口,其余的部署要么断开,要么明确只是预览。
不懂的代价
好几个版本同时在线,迟早分叉;哪个才是生产,连做它的 AI 都说错过,排查时总在错的那一份上改。
两类平台,各管一段
| 静态托管 | 应用托管 | |
|---|---|---|
| 代表 | Cloudflare Pages、Vercel | Railway、Render |
| 放什么 | 构建好的页面和文件 | 一直在线的后端程序、数据库 |
| 怎么收费 | 大多有很宽的免费额度 | 按运行时长和资源计费 |
| 要不要维护 | 几乎不用 | 要看日志、管重启、做升级 |
| 在这里的用法 | 个人网站、E2E 的前端 | E2E 的后端和数据库 |
一个常见的组合是前端放静态托管、后端放应用托管,中间用转发把它们接到同一个域名下(第 3 课)。这个组合本身没问题,问题出在"顺手":后端镜像里带上了前端,于是应用托管那边也多出了一个完整的网站。
一个网站,只有一个生产入口
把这两个项目所有能打开的地址列出来,是这样的:
| 地址 | 是什么 | 现在的状态 |
|---|---|---|
| www.felixwithai.com | 个人网站的生产入口 | 正常 |
| space.felixwithai.com | 个人网站的旧入口 | 301 到 www,保留路径与查询参数 |
| web-6eg.pages.dev | 同一次构建的平台默认地址 | 能打开,页面的 canonical 指向正式域名 |
| 分支名.web-6eg.pages.dev | 分支预览 | 能打开,平台会标记为不收录 |
| Vercel 上的部署 | 每次推送多构建的一份 | 需要登录才能看,但每次推送照样构建 |
| e2e.felixwithai.com | E2E 原来的正式域名 | 整站 301 到 /e2e |
| Railway 的默认域名 | 后端顺手托管的前端 | 仍能打开,停在 7 月的版本 |
每一行都要能回答:它是生产、预览,还是该删掉?答不上来的那一行,迟早会和生产分叉,或者在某次排查时把人带偏。
判断哪个才是生产,看域名指向谁(第 1 课);而不是看哪个平台发来了"部署成功"。
深潜canonical:告诉搜索引擎哪一份是正本+
同一份内容在好几个地址上都能打开时,页面头部的 <link rel="canonical"> 告诉搜索引擎"以这个地址为准",其余地址的权重会归到它身上。
写这节课时才发现,这个网站的 canonical 一直带着 .html 后缀,比如 /blog/loop-engineering.html,而这个地址在线上会被 308 跳转到不带后缀的版本:正本指向了一个跳转。原因是构建时每页生成一个 .html 文件,页面拿到的路径也带着后缀。现在已经改成和线上地址一致。
找茬
下面是两个项目里和部署入口有关的真实代码与配置(节选)。个人网站的生产环境在 Cloudflare Pages;E2E 的正式域名由 Cloudflare Pages 接住,后端在 Railway。找出制造了多余入口、或者在为错误的平台工作的地方。
这段 JS / Dockerfile / JSON 里埋了 5 处问题。点击你觉得有问题的行,至少找出 3 处再揭晓。
·第 2 行中危
挂不挂前端,看构建产物在不在
这一行的本意是“构建过前端,就顺便托管”。可镜像里永远有构建产物,于是后端永远在对外提供一整套前端。没有任何人有意做过这个决定。
该问的话:后端为什么要托管前端?这个行为是在哪里、由谁决定打开的?
·第 4 行低危
多出来的入口,对任何地址都回 200
后端的默认域名上,不存在的地址、
robots.txt、sitemap.xml全都返回首页和 200。这个入口一旦被搜索引擎发现,会被当成一整个网站收录。该问的话:后端的默认域名上,访问一个不存在的地址会返回什么?
·第 7 行中危
说是只管接口的镜像,装着整套前端
镜像构建时把前端也打包了进去。AI 后来还写过“railway up 只管 API”,可部署上去的一直是前端加后端,版本还停在 7 月。
该问的话:这个镜像里到底装了什么?部署上去的前端是哪个版本?
·第 10 行中危
转发的上游,自己也是一个完整的公开网站
转发脚本把
/api/交给这个地址,而这个地址自己也在对外提供整个网站。两边各自部署、各自更新,7 月 28 日就真的分叉了:一边新标题,一边旧标题。该问的话:除了正式域名,还有哪些地址能打开这个网站?它们是同一个版本吗?
·第 13 行低危
在给一个不是生产的平台写配置
迁移时 AI 认真改写了
vercel.json,可生产环境是 Cloudflare Pages,根本不读它。Vercel 的连接一直没断,每次推送照样构建一份没人访问的副本。该问的话:这个配置文件是给哪个平台的?那个平台现在还在用吗?
查看代码与答案(5 处问题)
1 // server/index.mjs(E2E 后端,节选)
2 if (existsSync(WEB_DIST)) {
3 app.use("/assets/*", serveStatic({ root: "./web/dist" }));
4 app.get("*", serveStatic({ path: "./web/dist/index.html" }));
5
6 # Dockerfile(E2E 后端镜像,节选)
7 COPY --from=web /build/dist ./web/dist
8
9 // web/public/_worker.js(前端转发脚本,节选)
10 const ORIGIN = "https://e2e-academy-production.up.railway.app";
11
12 # 个人网站的 vercel.json(迁移时改写)
13 { "cleanUrls": true, "trailingSlash": false }- 第 2 行 · 中危 · 挂不挂前端,看构建产物在不在:这一行的本意是“构建过前端,就顺便托管”。可镜像里永远有构建产物,于是后端永远在对外提供一整套前端。没有任何人有意做过这个决定。 该问的话:后端为什么要托管前端?这个行为是在哪里、由谁决定打开的?
- 第 4 行 · 低危 · 多出来的入口,对任何地址都回 200:后端的默认域名上,不存在的地址、
robots.txt、sitemap.xml全都返回首页和 200。这个入口一旦被搜索引擎发现,会被当成一整个网站收录。 该问的话:后端的默认域名上,访问一个不存在的地址会返回什么? - 第 7 行 · 中危 · 说是只管接口的镜像,装着整套前端:镜像构建时把前端也打包了进去。AI 后来还写过“railway up 只管 API”,可部署上去的一直是前端加后端,版本还停在 7 月。 该问的话:这个镜像里到底装了什么?部署上去的前端是哪个版本?
- 第 10 行 · 中危 · 转发的上游,自己也是一个完整的公开网站:转发脚本把
/api/交给这个地址,而这个地址自己也在对外提供整个网站。两边各自部署、各自更新,7 月 28 日就真的分叉了:一边新标题,一边旧标题。 该问的话:除了正式域名,还有哪些地址能打开这个网站?它们是同一个版本吗? - 第 13 行 · 低危 · 在给一个不是生产的平台写配置:迁移时 AI 认真改写了
vercel.json,可生产环境是 Cloudflare Pages,根本不读它。Vercel 的连接一直没断,每次推送照样构建一份没人访问的副本。 该问的话:这个配置文件是给哪个平台的?那个平台现在还在用吗?
快测
1. 你的静态博客,应该放在哪类平台上?
对了。构建好的页面和文件放静态托管,大多有很宽的免费额度,几乎不用维护;要一直在线的后端和数据库,才放应用托管。
你可能是这么想的:静态网站不需要一直运行的程序。放在应用托管上,要按运行时长付费,还得自己看日志、管重启、做升级。
你可能是这么想的:两份就会分叉,这正是这节课的事故:两边各自部署、各自更新,同一个网站同时跑着两个版本。
按网站的性质选平台:要一直运行的放应用托管,构建好就不变的放静态托管。
查看选项与答案
- A. 静态托管:页面构建好就不变,直接送出,几乎不用维护(正确)——构建好的页面和文件放静态托管,大多有很宽的免费额度,几乎不用维护;要一直在线的后端和数据库,才放应用托管。
- B. 应用托管:博客也需要一个一直在线的程序来响应每一次访问——静态网站不需要一直运行的程序。放在应用托管上,要按运行时长付费,还得自己看日志、管重启、做升级。
- C. 静态托管和应用托管各放一份,一边出问题就切换——两份就会分叉,这正是这节课的事故:两边各自部署、各自更新,同一个网站同时跑着两个版本。
按网站的性质选平台:要一直运行的放应用托管,构建好就不变的放静态托管。
2. 同一个网站在两个地址都能打开,内容一样。对搜索引擎来说,最重要的是什么?
对了。告诉搜索引擎以哪个地址为准,其余地址的权重会归到它身上。
你可能是这么想的:会被当成重复内容,权重被分散在两个地址上。
你可能是这么想的:两个地址各指各的,就等于两边都自称正本,搜索引擎还是不知道以哪个为准,权重照样被分散。
多个地址时,要明确说出哪一个是正本。
查看选项与答案
- A. 用 canonical 或 301,让其余地址指向正式的那一个(正确)——告诉搜索引擎以哪个地址为准,其余地址的权重会归到它身上。
- B. 把两个地址都提交给搜索引擎,让它们各自积累权重和排名——会被当成重复内容,权重被分散在两个地址上。
- C. 给每个地址各设一个指向自己的 canonical,各自收录——两个地址各指各的,就等于两边都自称正本,搜索引擎还是不知道以哪个为准,权重照样被分散。
多个地址时,要明确说出哪一个是正本。
3. 推送之后,你收到一条平台发来的“部署成功”。怎么确认线上真的更新了?
你可能是这么想的:正式域名也可能还在提供旧版本,它照样能正常打开,只是内容还是旧的。要看内容是不是新的,不能只看能不能打开。
对了。线上以域名指向的那个平台为准,看它实际返回了什么。
你可能是这么想的:发通知的平台未必是生产。这次事故里,Railway 的默认域名上是新标题,正式域名上却还是旧的。
部署成功不等于线上更新:去正式域名上确认。
查看选项与答案
- A. 打开正式域名,页面能正常显示、没有报错就说明更新了——正式域名也可能还在提供旧版本,它照样能正常打开,只是内容还是旧的。要看内容是不是新的,不能只看能不能打开。
- B. 打开正式域名,对照标题和内容,确认已经是新版本(正确)——线上以域名指向的那个平台为准,看它实际返回了什么。
- C. 打开发通知的平台给的默认地址,确认内容已经是新版本——发通知的平台未必是生产。这次事故里,Railway 的默认域名上是新标题,正式域名上却还是旧的。
部署成功不等于线上更新:去正式域名上确认。
判断时刻
个人网站的仓库同时连着 Vercel 和 Cloudflare Pages,每次推送两边各部署一份;域名指向 Cloudflare。AI 给了三个方案。
你会选哪一个?
考察:备份还是负担
什么都不用做。代价是每次推送都多一份构建、多一条可能误导人的“部署成功”。这个网站迁移时,AI 就是照着 Vercel 写的部署说明,差点只看它的结果就合并了。
考察:一个生产入口
几分钟的后台操作,从此只有一个平台、一种通知、一份配置。唯一的代价是:将来真要换平台时,得重新接一次。
考察:换平台的成本
同样只剩一个入口,但要迁移构建配置、重新验证所有网址的跳转和 404、处理证书,工作量最大;而原来的平台并没有出问题,换的理由并不充分。
多一个入口,不是多一份保险,而是多一个会分叉、会误导人的版本。每个入口都要有明确的身份:生产、预览,或者该删掉。
三个选项各自的代价
- A. 保持现状,多一份当备份——考察备份还是负担:什么都不用做。代价是每次推送都多一份构建、多一条可能误导人的“部署成功”。这个网站迁移时,AI 就是照着 Vercel 写的部署说明,差点只看它的结果就合并了。
- B. 断开 Vercel,只留 Cloudflare Pages——考察一个生产入口:几分钟的后台操作,从此只有一个平台、一种通知、一份配置。唯一的代价是:将来真要换平台时,得重新接一次。
- C. 把域名改指向 Vercel,断开 Cloudflare——考察换平台的成本:同样只剩一个入口,但要迁移构建配置、重新验证所有网址的跳转和 404、处理证书,工作量最大;而原来的平台并没有出问题,换的理由并不充分。
多一个入口,不是多一份保险,而是多一个会分叉、会误导人的版本。每个入口都要有明确的身份:生产、预览,或者该删掉。
带走
下次让 AI 做这件事时,问它
- 这个网站现在能从哪些地址打开?逐个列出:哪个是生产、哪个是预览、哪个该断开。
- 这个仓库连着哪些托管平台?每次推送会触发几个部署?
- 后端除了提供接口,是不是也在托管页面?这是有意的吗?
- 同一份内容有多个地址时,有没有用 canonical 或 301 指明哪一个是正本?
自己验证
- 对每个入口执行
curl -sI,看状态码、server响应头和页面标题是否一致。 - 在代码仓库的提交记录上看检查结果,数一数每次推送触发了几个平台。
- 查看页面源代码里的
<link rel="canonical">,确认它指向正式域名,而且不会再被跳转。
每个入口都要有明确的身份:生产、预览,或者该删掉。
