博客域名迁移复盘:从 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、sitemapRSS 都会指向旧地址,新域名下的统计 API 还会被 CORS 拒绝。于是我做了第一波代码迁移。

文件改动
astro.config.mjssitehttps://rodneyzhang.com
src/components/SEO.astrofallback site 同步
public/robots.txtSitemap 地址指向新域名
worker/src/index.jsALLOWED_ORIGIN由单值改为逗号分隔的多域名列表,CORS 按请求Origin回显匹配项;CSRF 校验由等号比较改为includes/some
worker/wrangler.tomlALLOWED_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 同处一个生态,绑定自定义域名时不再有任何第三方校验障碍。

落地步骤

  1. 创建 Pages 项目wrangler pages project create --production-branch=main,得到 rodneyzhang.pages.dev
  2. CI 双平台部署。在 .github/workflows/deploy.yml 新增一步 wrangler pages deploy dist --project-name=rodneyzhang --branch=main,用仓库 Secrets CLOUDFLARE_API_TOKEN + CLOUDFLARE_ACCOUNT_ID。同时删除 public/CNAME(域名改由 Cloudflare Pages 托管,GitHub Pages 不再绑定,消除设置页的持续警告;旧的 rd-zzzz.github.io 默认域继续保留)。提交 0186382
  3. 控制台绑自定义域名。在 Cloudflare 控制台把 rodneyzhang.com 绑到 Pages 项目,删除旧的 GitHub A 记录 185.199.x,新增 CNAME → rodneyzhang.pages.dev
  4. 验证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 10042wrangler.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.tomlroutes = [{ pattern = "stats.rodneyzhang.com", custom_domain = true }]ALLOWED_ORIGIN 扩为 rodneyzhang.com + www.rodneyzhang.com + rd-zzzz.github.io
  • wrangler 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 记录
  • 发件也有两条免费的路
    1. Gmail「Send mail as」代发(选「通过 Gmail 发送」模式,走 Google 的 SPF/DKIM,无需改 DNS)
    2. 腾讯企业邮箱免费版(完整收发,MX 归腾讯,与 Email Routing 二选一)

发信的本质是借 SMTP 服务器,再用域名的 SPF/DKIM/DMARC 授权,不需要自建邮件服务器。