Hexo 换域名后 canonical 和 sitemap 还指着旧域?完整迁移清单
换个域名,比想象中麻烦
博客从 blog.demo.fan 迁移到 blog.demohappy.top。这件事看起来就是把 _config.yml 里的 url: 改一行、重新部署。
改完确实生效了——页面能打开,只是有两件事没跟着走。
第一件:搜索结果里点进来,<link rel="canonical"> 还指着 blog.demo.fan。这意味着搜索引擎会认为新域名上的每一页都是旧域名内容的副本,新域名的权重永远是零。
第二件:sitemap.xml 里 149 条 URL,全部是旧域。
为什么会这样:三类引用的生命周期不同
把「站点里出现的域名」按来源分类,会发现它们的更新方式完全不同:
| 类型 | 来源 | 改 url: 生效吗 |
|---|---|---|
| canonical / og:url / JSON-LD / sitemap | 模板运行时读 config.url |
✅ 重新生成即生效 |
copyright_url(每篇 front-matter) |
文章里的字面量 | ❌ 永远不会变 |
robots.txt 的 Sitemap: 行 |
源文件里的手写字符串 | ❌ 永远不会变 |
| about 页、友链、正文里的绝对链接 | 手写 | ❌ 永远不会变 |
| 部署脚本里拼的地址 | 代码里的字面量 | ❌ 永远不会变 |
所以「改了 config 就完事」是错的。真正容易出事的是第三类:写死在内容里的绝对地址。
而且这里有个更隐蔽的坑:不止一处硬编码。当时博客每天有一个自动写文章的脚本,它会给新文章写 front-matter:
1 | copyright_url: https://blog.demo.fan/posts/xxxx/ |
这个地址是脚本里拼的字符串常量。结果就是——config 改完了,sitemap 也指向新域了,但每天新增的文章还在把旧域名写回 front-matter。如果不修脚本,几周后全站又会有一批旧域链接。
判定标准:任何「要拼站点绝对地址」的脚本,都必须从
_config.yml读url:,不允许硬编码。这是单一真源原则,和下面要说的顺序原则一样重要。
六步迁移清单
按顺序执行,每一步都有验证方式。
第 0 步(最重要):先确认新域名已经能打开站点
顺序不能颠倒。 如果先改 config.url、而新域名还没绑定好,会有一段窗口期:canonical、sitemap、og:url 全部指向一个 NXDOMAIN 或 403 的地址。搜索引擎此时来抓,看到的是「所有页面都声明自己的正版地址不存在」——这在 SEO 上是明确的负面信号。
正确顺序:
1 | 新域名解析完成 → 能打开站点 → 改 config.url → 重新构建部署 |
第 1 步:改 _config.yml 的 url
1 | # URL |
注意 subtitle 如果也写了域名,要单独判断要不要改(它会影响部分主题的 <title> 拼接)。这个字段建议显式决定,不要顺带改掉,否则首页标题会意外变化。
第 2 步:改 source/robots.txt 的 Sitemap 行
1 | User-agent: * |
这行不跟着 config 走,必须手改。改完检查站点根部的 /robots.txt 确实返回了新域名——有些托管平台会覆盖这个文件,需要单独确认。
第 3 步:批量改 front-matter 的 copyright_url
1 | grep -rl "blog.demo.fan" source/_posts/*.md | wc -l |
如果输出不是 0,写个脚本批量替换。关键约束是只改结构性引用,不碰正文:正文里可能正写着「本站曾用域名 blog.demo.fan」这类历史叙述,改了就把历史篡改成了错误信息。
1 | import re |
第 4 步:修掉生成脚本里的硬编码
找出所有会写文件、且包含域名的地方:
1 | grep -rn "blog\.demo\.fan" tools/ .workbuddy/scripts/ 2>/dev/null |
把里面的字面量换成从配置读取:
1 | def _site_url() -> str: |
这样下次换域名,只要改 _config.yml 一处。
第 5 步:hexo clean 再构建
这一步不能省。hexo g 是增量构建,不会删除已经不存在页面的旧产物。如果之前调整过分页数量、或者改过永久链接格式,public/ 里会留下一批孤儿页面——它们的 canonical 指向旧域,但依然会被部署上去。
1 | npx hexo clean && npx hexo g |
第 6 步:全站核对
构建完,直接在产物里搜旧域名:
1 | grep -rl "blog.demo.fan" public/ | wc -l |
要的结果是 0。 如果不为 0,看是哪些文件:
1 | grep -rl "blog.demo.fan" public/ | head -20 |
通常是三类:漏改的 front-matter、某个手写页面、或者主题模板里藏的默认值。
迁移后的收尾
新域名上线只是开始,还要处理旧域名的尾巴:
给旧域做 301 跳转。 旧域名在搜索引擎里可能已经有索引和少量外链,直接弃用会浪费掉。用 Cloudflare Redirect Rule 之类的规则把 旧域/* 永久跳转到 新域/$1。不要用 302——302 不传递权重。
在 Google Search Console 里提交新域名。 新域名需要单独验证、单独提交 sitemap。如果旧域名也验证过,可以在 GSC 里用「地址更改」工具声明迁移,加速权重转移。
旧域的 sitemap 要撤掉。 否则搜索引擎会持续从旧域发现内容,而你希望流量都到新域。
顺序原则收口
整个迁移过程中真正会出错的地方,都不是技术难点,而是顺序:
- 新域能访问 之前,不要改
config.url - 硬编码的脚本 不修,每天的新内容都会把旧域写回来
hexo clean不跑,孤儿页面会带着旧 canonical 一起上线
三条都是「看起来做了、其实没做干净」的类型。验证方式统一为一句 grep——产物里旧域名出现次数必须是 0。
这次迁移的完整执行记录在 记录一次博客主域名迁移的全过程,迁移脚本本身在仓库的 tools/migrate_domain.py,幂等,可以反复跑。