博客域名迁移复盘:从 rd-zzzz.github.io 到 rodneyzhang.com
一、为什么要离开 github.io
之前这一段时间,这个博客一直挂在 rd-zzzz.github.io 上,它是 GitHub Pages 给的免费二级域名。可以正常运行,但有几个绕不开的问题。
1. 基础设施的 DNS 解析权不在自己手里
GitHub Pages 用的 CDN 是 Fastly,在国内的访问速度时快时慢,甚至有时候需要挂代理,而 github.io 的 DNS 在 GitHub 手里,无法解决。
2. Worker 的 *.workers.dev 在大陆被干扰
博客的访问量统计跑在 Cloudflare Worker 上,原始地址是 blog-stats.zrd.workers.dev。实测直连会 TLS 超时 10 秒,前端统计长期假死。根因是 *.workers.dev 这个公共后缀在当前网络环境里被干扰,而 github.io 的 DNS 又在 GitHub 手里,无法把 Worker 绑到自己的子域上来解决问题。
而根域名换成自己的,就很容易能解决这些问题。
二、前置教训,一次失败的伪迁移
在真正买域名之前,我先试过一条捷径,把 Pages 仓库从 rd-zzzz.github.io 改名为 Rodney.github.io,想拿到 https://rodney.github.io/ 这个根域名。我只改了 9 个源码文件里的旧域名字符串就推上去了。
结果全站 404。根因是 GitHub Pages 的一条路径规则。
只有仓库名恰好等于
<账号>.github.io时,GitHub 才从根路径https://<账号>.github.io/服务,否则一律当作项目页,落在https://<账号>.github.io/<仓库名>/子路径下。
我的 GitHub 账号仍是 rd-zzzz,所以改名后的 Rodney.github.io 被当成项目页,线上路径变成 https://rd-zzzz.github.io/Rodney.github.io/。此外,workflow 里的 external_repository 指向了不存在的 Rodney/Rodney.github.io,部署根本推不进去。
要拿到干净的 rodney.github.io,唯一的正解是把 GitHub 账号改名,而那会影响所有仓库路径与一堆外部引用,成本太高。我最终回退到了 rd-zzzz.github.io。
看来二级域名的根路径是平台给的,并不方便随意自定义。如果想彻底掌控就需要购买私有的域名。
三、购买与 DNS 托管,为什么选 Cloudflare
2026-08-02,我买下了 rodneyzhang.com。本以为 .com 顶级域名会比较贵,没想到在 Cloudflare 买一年只要 10.46 美元,后续每年续费价格不变。对比阿里云首年 85 元人民币、续费 95 元,其实后者也可以接受。便宜的根源在于 rodneyzhang 足够冷门也足够长,域名不涉及其它用户的竞争便会很廉价。

我在 Cloudflare 上完成购买,并且把 DNS 解析也托管在 Cloudflare(开启橙云代理)。选它而不只是把它当注册商,是因为后面所有需求都能收进同一个生态。
- CDN / 代理(橙云)
- Cloudflare Pages(静态站点托管)
- Cloudflare Workers(统计 API)
- Email Routing(免费域名邮箱)
域名、DNS、托管、边缘函数、邮箱全在同一家,后面每一步的对接成本都趋近于零。
四、代码层迁移,先把站内引用全部换掉
买域名不是改个 DNS 就 OK。如果源码里还硬编码着旧域名,canonical、Open Graph、sitemap、RSS 都会指向旧地址,新域名下的统计 API 还会被 CORS 拒绝。于是我做了第一波代码迁移。
| 文件 | 改动 |
|---|---|
astro.config.mjs | site→https://rodneyzhang.com |
src/components/SEO.astro | fallback site 同步 |
public/robots.txt | Sitemap 地址指向新域名 |
worker/src/index.js | ALLOWED_ORIGIN由单值改为逗号分隔的多域名列表,CORS 按请求Origin回显匹配项;CSRF 校验由等号比较改为includes/some |
worker/wrangler.toml | ALLOWED_ORIGIN = "https://rodneyzhang.com, https://rd-zzzz.github.io"(保留旧域名,GitHub Pages 默认域仍可访问) |
README.md/docs/dual-repo-setup.md | 站点链接更新 |
设计要点是,Worker 的 CORS 白名单必须支持多域名回显。旧逻辑是固定回显某个 ACAO,在双域名并存期会出问题。正确做法是把允许的 Origin 存成列表,响应时 if (allowed.includes(origin)) header('Access-Control-Allow-Origin', origin),并对非白名单 Origin 直接 403。既解决了跨域,又保住了 CSRF 防护。
构建验证。npm run build 85 页全量替换新域名、旧域名零残留。提交 git 并推送。
五、第一道坎,GitHub Pages 与 Cloudflare 代理的冲突
最初我试着在 GitHub Pages 设置里直接绑定已经购买的私有域名,立刻撞墙。
- GitHub Pages 绑自定义域名时,要求 DNS 的 A 记录指向 GitHub 的 IP(
185.199.x.x),并做校验 - 我的域名 DNS 托管在 Cloudflare 且开了橙云代理,Cloudflare 回给 GitHub 的是它自己的 IP,校验失败,页面 404
- 即便绕过,Fastly CDN 在国内的稳定性依旧没解决
矛盾出在 Cloudflare 代理(你需要它做 CDN、缓存、安全)与 GitHub Pages 的“DNS 必须直指 GitHub IP”校验天然冲突。二者只能取其一。
六、转向 Cloudflare Pages,全 Cloudflare 生态打通
既然 DNS 已经在 Cloudflare,最干净的方案是把站点本身也搬到 Cloudflare Pages,让域名、CDN、Worker 同处一个生态,绑定自定义域名时不再有任何第三方校验障碍。
落地步骤。
- 创建 Pages 项目。
wrangler pages project create --production-branch=main,得到rodneyzhang.pages.dev - CI 双平台部署。在
.github/workflows/deploy.yml新增一步wrangler pages deploy dist --project-name=rodneyzhang --branch=main,用仓库 SecretsCLOUDFLARE_API_TOKEN+CLOUDFLARE_ACCOUNT_ID。同时删除public/CNAME(域名改由 Cloudflare Pages 托管,GitHub Pages 不再绑定,消除设置页的持续警告;旧的rd-zzzz.github.io默认域继续保留)。提交0186382 - 控制台绑自定义域名。在 Cloudflare 控制台把
rodneyzhang.com绑到 Pages 项目,删除旧的 GitHub A 记录185.199.x,新增CNAME → rodneyzhang.pages.dev - 验证。
https://rodneyzhang.com返回 200 加真实内容,DNS 解析到 Cloudflare IP(104.21.88.156/172.67.185.199)
到这一步,架构变成这样。源码推 GitHub,Actions 同时部署 GitHub Pages(原有的github.io保底)和 Cloudflare Pages(主力,rodneyzhang.com 指向它)。
七、收尾,那些构建不会自动改的死角
自动化部署只重建 dist/,有几个静态文件与边缘服务不会被自动修正,必须手动兜底。
1. robots.txt 的 Sitemap 仍指向旧域名
public/robots.txt 是静态文件,构建不替换。改成 https://rodneyzhang.com/sitemap-index.xml,提交 b594a86。
2. 统计 Worker 没重新部署
GitHub Actions 只部署 Pages,不部署 Worker。线上 Worker 的 ALLOWED_ORIGIN 还是旧域名,新域名下的统计被 CORS 拒。
- 第一次
wrangler deploy报错code 10042,wrangler.toml里的 KV namespace id 还是占位符YOUR_KV_NAMESPACE_ID。用wrangler kv namespace list查出真实 id 填入后部署成功 - 验证。用
rodneyzhang.com做 Origin,响应回显新域名;换成evil.com发 POST,返回 403;KV 里的历史访问量完整保留 - 决策。KV namespace id 属资源标识符而非密钥,随仓库入库(Blog 仓库私有),免去下次手动替换;真正的
STATS_TOKEN仍走 Secret 不入库
3. 旧域名自动 301
GitHub Pages 对旧 rd-zzzz.github.io 自动 301 重定向到 rodneyzhang.com,无 SEO 重复内容问题。
4. 站点验证文件被 pretty URLs 吞掉
Cloudflare Pages 会自动剥离 .html(308 跳转),百度与 Google 要求精确 .html 验证文件返回 200,于是验证失败。改法是在 SEO.astro 的全局 head 里加 meta 标签验证。
<meta name="baidu-site-verification" content="codeva-LFlrP7rYFV">
<meta name="google-site-verification" content="google8a09aa7dbeeba943">
八、统计 API 也拥有自己的域 stats.rodneyzhang.com
回到第一节提到的那个老问题,*.workers.dev 在大陆被干扰,前端统计一直假死。08-04 发现 Worker 还挂在 *.workers.dev 上,直连 10 秒超时。既然 DNS 已在 Cloudflare,根治办法就是给 Worker 绑一个自己的子域。
- 前端
STATS_API改为https://stats.rodneyzhang.com worker/wrangler.toml加routes = [{ pattern = "stats.rodneyzhang.com", custom_domain = true }],ALLOWED_ORIGIN扩为rodneyzhang.com + www.rodneyzhang.com + rd-zzzz.github.iowrangler deploy成功,自定义域绑定确认
验证。直连 https://stats.rodneyzhang.com/api/views?slug=_site 返回 200,耗时 0.64 秒(对比旧 workers.dev 直连 10 秒超时)。能打开博客,就能加载统计,因为主域和统计子域走的是同一套 Cloudflare CDN。
wrangler的 OAuth Token 没有 zone 权限,DNS 记录由 Cloudflare 自动创建,公共 DNS 有数分钟负缓存窗口属正常,等传播即可。
九、附带的礼物,免费域名邮箱
有了自己的域名和 Cloudflare DNS,顺手开了 Email Routing。在关于页加上 [email protected] 的可点击 mailto 链接。
- 收件免费。Cloudflare 自动写 MX/SPF,转发到 Gmail,不碰博客的 CNAME/A 记录
- 发件也有两条免费的路。
- Gmail「Send mail as」代发(选「通过 Gmail 发送」模式,走 Google 的 SPF/DKIM,无需改 DNS)
- 腾讯企业邮箱免费版(完整收发,MX 归腾讯,与 Email Routing 二选一)
发信的本质是借 SMTP 服务器,再用域名的 SPF/DKIM/DMARC 授权,不需要自建邮件服务器。