一些 WordPress 网站在最初部署时,会直接放在主站的一个子目录下,例如:

https://www.example.test/articles/

随着网站长期运行,博客逐渐成为一个相对独立的应用,此时将其迁移到独立子域名:

https://blog.example.test/

通常能够获得更清晰的系统边界。

但这并不是简单修改一个链接。

对于已经运行多年的 WordPress,子目录迁移到子域名通常会涉及:

  • DNS
  • TLS
  • 前置反向代理
  • Apache/Nginx VirtualHost
  • WordPress homesiteurl
  • .htaccess
  • 数据库中的绝对 URL
  • PHP serialized data
  • WordPress GUID
  • Cookie
  • 页面缓存
  • Object Cache
  • Permalink
  • 旧地址退役
  • 完整回退方案

本文记录一种风险相对较低的迁移方法:

公网地址迁移到新的子域名,但 WordPress 的物理目录保持不变。

这样能够最大限度减少与域名迁移无关的文件系统变化。


一、迁移目标

假设原来的 WordPress 地址为:

https://www.example.test/articles/

希望迁移到:

https://blog.example.test/

WordPress 原本安装在服务器上的某个目录:

/var/www/site/wordpress-data/

迁移后这个目录仍然保持不变。

也就是说,只改变:

外部访问身份

而不改变:

WordPress 实际文件位置

最终架构大致为:

Internet
   │
   │ HTTPS
   ▼
Reverse Proxy
   │
   │ HTTP / internal network
   ▼
Apache
   │
   ▼
WordPress physical directory

新的子域名直接映射到 WordPress 的现有物理目录。

这种方式有一个很重要的优势:

域名迁移与文件系统迁移彻底解耦。


二、不要先修改 WordPress

比较安全的迁移顺序并不是:

修改 WordPress URL
→ 再配置服务器

而应该反过来:

准备 DNS
→ 准备 TLS
→ 准备反向代理
→ 准备 Apache VirtualHost
→ 验证新 Host 链路
→ 修改 WordPress

原因很简单。

一旦 WordPress 的:

home
siteurl

被修改,它就可能立即开始:

  • 生成新域名链接;
  • 将后台重定向到新域名;
  • 设置新域名 Cookie;
  • 返回新域名 REST URL。

如果此时新域名的 DNS、TLS 或 Web Server 还没有准备好,站点就会进入半迁移状态。

因此应该首先让:

https://blog.example.test/

具备正常到达 WordPress 后端的能力。


三、先确认 WordPress 类型

迁移之前首先确认:

Single Site

还是:

Multisite

这两种 WordPress 的域名结构和数据库处理方式差异很大。

普通 Single Site 的迁移相对直接。

如果启用了 Multisite,则不应该简单照搬本文流程。


四、检查 home 和 siteurl

WordPress 中最核心的站点 URL 是:

home
siteurl

原来可能是:

home    = https://www.example.test/articles
siteurl = https://www.example.test/articles

迁移后的目标为:

home    = https://blog.example.test
siteurl = https://blog.example.test

同时还应该检查:

WP_HOME
WP_SITEURL

是否在 wp-config.php 中被显式定义。

如果这两个常量已经写死,那么仅修改数据库并不会改变最终结果。


五、检查 Cookie 设置

从:

主域名 + 子目录

迁移到:

独立子域名 + 根路径

以后,WordPress 登录 Cookie 的作用范围也会发生变化。

应当检查是否手工定义过:

COOKIE_DOMAIN
COOKIEPATH
SITECOOKIEPATH
ADMIN_COOKIE_PATH

如果没有人为固定这些值,WordPress 通常可以根据新的站点 URL 自动调整。

迁移完成以后仍然应该实际测试:

登录
后台
登出
重新登录

因为登录页能够显示,并不等于 Cookie 行为一定正常。


六、DNS 与 TLS 应先完成

新子域名应该首先拥有正确的 DNS 记录:

blog.example.test
→ public reverse proxy

然后准备覆盖该 Host 的 TLS 证书。

可以采用:

  • 独立证书;
  • SAN 证书;
  • wildcard 证书。

关键原则是:

WordPress 开始使用新域名以前,新域名的 HTTPS 必须已经能够正常建立连接。

完成这一阶段以后,可以先测试:

TLS handshake
Host routing
reverse proxy
backend connection

而暂时不修改 WordPress 数据库。


七、前置反向代理增加新的 Host

如果架构中使用 Nginx 作为 HTTPS 前端,可以为新的博客域名建立独立 server

示意配置:

server {
    listen 443 ssl;
    server_name blog.example.test;

    # TLS settings ...

    location / {
        proxy_pass http://BACKEND_SERVER;

        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

        proxy_set_header X-Forwarded-Proto https;
        proxy_set_header X-Forwarded-Host $host;
        proxy_set_header X-Forwarded-Port 443;
    }
}

其中尤其重要的是:

Host
X-Forwarded-Proto

后端必须知道:

用户访问的 Host

以及:

外部实际使用 HTTPS

否则可能出现:

  • HTTP/HTTPS 重定向循环;
  • WordPress 生成 HTTP URL;
  • Secure Cookie 判断异常;
  • 后台跳转错误。

修改 Nginx 后应先:

nginx -t

确认无误,再:

reload

而不是直接 restart。


八、Apache 使用独立 VirtualHost

后端 Apache 也应该为博客子域名建立独立 VirtualHost。

例如:

<VirtualHost *:80>
    ServerName blog.example.test

    DocumentRoot /var/www/site/wordpress-data

    <Directory /var/www/site/wordpress-data>
        AllowOverride All
        Require all granted
    </Directory>
</VirtualHost>

这样:

blog.example.test/

直接对应 WordPress 根目录。

这比继续通过:

/articles/

这样的 Alias 或子路径模拟 WordPress 根路径更加清晰。

同时,WordPress 的实际目录完全不用移动。


九、反向代理环境中的 HTTPS 识别

如果公网层使用 HTTPS,而后端 Apache 接收到的是 HTTP,那么 PHP 本身可能认为当前连接不是 HTTPS。

因此通常需要根据:

X-Forwarded-Proto: https

恢复 HTTPS 环境。

例如 Apache 可以根据代理 Header 设置相应环境变量。

迁移前应先确认这一机制已经正确工作。

否则 WordPress 可能产生:

HTTPS
↓
HTTP
↓
HTTPS

这样的重定向循环。


十、子目录模式的 .htaccess 需要调整

WordPress 原来运行在:

/articles/

时,.htaccess 可能包含:

RewriteBase /articles/
RewriteRule . /articles/index.php [L]

迁移到独立子域名以后,WordPress 对公网而言位于根路径:

/

因此 Rewrite 通常需要调整为:

RewriteBase /
RewriteRule . /index.php [L]

这里有一个非常重要的边界:

只调整 WordPress 的 URL 根路径,不要顺便修改 permalink 结构。


十一、不要同时重构 Permalink

假设迁移前的 permalink 为:

/index.php/%year%/%monthnum%/%day%/%postname%/

那么域名迁移以后,最安全的结果仍然是:

https://blog.example.test/index.php/...

而不是趁机改成:

https://blog.example.test/...

去掉 index.php 涉及的是另一套问题:

  • rewrite 规则;
  • permalink structure;
  • 历史链接;
  • Feed;
  • REST endpoint;
  • 搜索引擎 URL;
  • 额外 301。

一次迁移只解决一个核心问题,可以显著降低排障复杂度。


十二、数据库里的 URL 远比 home/siteurl 多

WordPress 运行多年以后,旧域名通常分布在很多地方,例如:

wp_posts.post_content
wp_postmeta.meta_value
wp_options.option_value
wp_users.user_url

以及:

  • Gutenberg 内容;
  • 主题设置;
  • Widget;
  • 菜单;
  • 附件;
  • 插件配置;
  • CDN 设置;
  • RSS enclosure。

因此只修改:

home
siteurl

是不够的。


十三、为什么不能直接用 SQL REPLACE

WordPress 数据库中经常存在 PHP serialized data。

例如某些 option 的逻辑结构类似:

a:2:{
    ...
    s:N:"https://old.example.test/...";
}

其中:

N

记录字符串长度。

如果直接通过:

REPLACE()

或者对 SQL dump 使用普通文本:

sed

进行全局替换,字符串长度可能不再正确,从而损坏序列化数据。

因此更适合使用:

WP-CLI search-replace

它能够处理 WordPress 常见 serialized data。


十四、先执行 Dry Run

正式替换以前应该先执行:

--dry-run

示意:

wp search-replace \
  'https://www.example.test/articles' \
  'https://blog.example.test' \
  --all-tables-with-prefix \
  --skip-columns=guid \
  --precise \
  --recurse-objects \
  --dry-run

需要检查:

  • 哪些表会发生修改;
  • 替换数量;
  • 是否出现异常插件表;
  • 是否和迁移前的数据库审计相符。

只有 dry-run 结果符合预期,才进行正式操作。


十五、不要修改 WordPress GUID

尤其需要注意:

wp_posts.guid

即使 GUID 中仍然保留旧域名,也不应该把它当作普通页面 URL 全部替换。

因此可以明确使用:

--skip-columns=guid

将 GUID 排除在普通 URL 迁移之外。

这是域名迁移中很容易发生误操作的地方。

数据库中仍然存在旧域名字符串,并不自动意味着迁移不完整。

首先要判断:

这个字段究竟是不是应该修改。

十六、正式执行 Search-Replace

确认 dry-run 后,再执行正式替换:

旧:
https://www.example.test/articles

新:
https://blog.example.test

保持与 dry-run 相同的参数,只去掉:

--dry-run

执行后再次 dry-run。

理想结果应该类似:

0 replacements remaining

同时单独确认:

home
siteurl

已经变成新子域名。


十七、清理缓存

数据库已经修改完成,并不代表浏览器马上看到的页面一定完全正确。

WordPress 常见缓存包括:

Page Cache
Object Cache
Redis
CDN
Browser Cache

迁移以后至少应该处理:

Object Cache
Page Cache

然后重新访问新域名,让新的页面缓存重新生成。

否则可能出现:

数据库已经没有旧 URL
但是网页 HTML 仍然偶尔返回旧域名

这通常不是数据库迁移失败,而是缓存。


十八、正式验证新域名

切换完成以后,不要只测试首页。

至少应该验证以下对象。

首页

https://blog.example.test/

登录页

/wp-login.php

后台

/wp-admin/

REST API

/wp-json/

Feed

根据当前 permalink 结构验证实际 Feed 地址。

文章

随机选择多篇不同时期的文章。

媒体

检查:

/wp-content/uploads/...

静态资源

检查:

CSS
JavaScript
favicon
图片
字体

Cookie

实际完成:

登录
后台操作
登出
重新登录

测试。


十九、旧 URL 的两种处理策略

WordPress 新域名稳定以后,旧:

https://www.example.test/articles/

还需要决定是否继续存在。

有两种合理策略。

方案 A:保留 301

适合:

  • 有较多外部链接;
  • 搜索引擎已经收录;
  • 希望历史链接长期可用。

例如:

https://www.example.test/articles/index.php/old-post/?page=2

跳转到:

https://blog.example.test/index.php/old-post/?page=2

必须保留:

path
query string

方案 B:彻底退役旧 namespace

如果旧路径不再需要兼容,可以让前置 Nginx 直接返回:

404

例如:

location = /articles {
    return 404;
}

location ^~ /articles/ {
    return 404;
}

最终形成:

blog.example.test/*
→ WordPress

www.example.test/articles*
→ 404

这样旧路径甚至不会进入后端 Apache。


二十、为什么直接返回 404 比放任后端处理更干净

如果旧 URL 对应服务器上仍然存在的真实目录,那么即使 WordPress 已迁移,Apache 仍可能执行目录规范化。

例如:

/articles

自动变成:

/articles/

反向代理环境下甚至可能生成不希望出现的协议或 Host。

如果明确决定废弃旧路径,那么在最前面的反向代理层直接:

return 404

通常更加简单、确定。


二十一、主站入口最后再切换

如果主站首页存在一个:

Blog

链接,不建议在迁移第一步就修改。

更安全的顺序是:

DNS/TLS
↓
Reverse Proxy
↓
Apache VirtualHost
↓
WordPress Rewrite
↓
数据库
↓
缓存
↓
新站完整验证
↓
旧 URL 策略
↓
主站 Blog 链接

只有新博客完全正常以后,再把主站入口切换到新的子域名。

这样能够避免用户提前进入一个尚未完成迁移的站点。


二十二、配置文件修改必须有回退点

生产环境配置发生变化以前,应保留修改前版本。

典型对象包括:

Nginx 配置
Apache VirtualHost
.htaccess
主站首页

数据库则应该另外建立完整 dump。

但备份管理中还有一个很容易被忽视的原则:

管理员自己的备份命名规则,不能强加到应用自己管理的文件上。

例如应用本身可能产生:

*.bak
installer
archive
backup
snapshot
cache

这些名称不一定代表“管理员手工备份”。

它们可能是应用运行状态的一部分。

因此批量整理文件以前必须先判断:

文件是谁生成的?
属于哪个应用?
当前是否仍然被应用管理?

不能仅根据:

.bak

这样的扩展名判断性质。


二十三、配置文件可以按应用命名

完成迁移以后,Web Server 配置没有必要全部按照域名命名。

例如 Apache 可以采用:

sites-available/
├── main-site.conf
├── wordpress.conf
└── cloud-app.conf

分别对应:

主站
WordPress
文件云应用

WordPress 配置文件内部再写:

ServerName blog.example.test

这样配置文件表达的是:

这个配置属于哪个应用

ServerName 表达:

这个应用当前通过哪个域名访问

两者职责明确。

以后即使域名发生变化,也不一定需要再次重命名配置文件。


二十四、物理目录同样不用追求改名

同理:

公网:
blog.example.test

并不要求:

物理目录:
/var/www/html/blog/

WordPress 完全可以继续位于历史目录:

/var/www/site/wordpress-data/

只要:

Apache DocumentRoot
权限
Rewrite
WordPress URL

全部正确,目录名称本身并没有技术意义。

为了“看起来一致”而迁移整个 WordPress 目录,反而会增加:

  • 文件移动;
  • 权限变化;
  • 路径硬编码;
  • 插件依赖;
  • 缓存问题;
  • 回滚难度。

二十五、一个更清晰的最终架构

经过迁移以后,可以形成类似:

                    Internet
                       │
                 Reverse Proxy
                  /          \
                 /            \
                ▼              ▼
       www.example.test   blog.example.test
                │              │
                └──────┬───────┘
                       ▼
                    Apache
                  /         \
                 ▼           ▼
             Main Site    WordPress

不同应用通过不同 Host 暴露。

同时保持后端文件系统相对稳定。

这种架构通常比:

/example-app-1/
/example-app-2/
/example-app-3/

全部堆在一个主域名下面更容易维护。


二十六、完整回退方案

正式迁移前,至少应该准备:

数据库完整备份
Reverse Proxy 配置备份
Apache 配置备份
.htaccess 备份
主站入口页面备份

如果迁移失败,可以按相反方向恢复:

取消新入口或旧 URL 新规则
↓
恢复 Reverse Proxy
↓
恢复 Apache
↓
恢复 .htaccess
↓
恢复数据库
↓
清理缓存
↓
恢复主站入口
↓
验证旧站

尤其需要注意:

数据库和 Web Server 路由必须作为一个整体回退。

如果数据库已经恢复到旧域名,而 Web Server 仍然只提供新 Host,站点仍然无法正常工作。


二十七、迁移结束后不要立即删除备份

完成技术验证以后,最好暂时保留迁移前回退点。

先进行一段人工验收:

  • 登录;
  • 后台;
  • 媒体库;
  • 多篇文章;
  • 上传资源;
  • Feed;
  • REST;
  • 手机端;
  • PC 端;
  • 缓存重新生成。

确认不存在隐蔽问题以后,再单独整理此次产生的迁移备份。

不要在迁移成功的同一个动作中自动删除回退资产。


总结

将 WordPress 从:

主域名/子目录/

迁移到:

独立子域名/

本质上不是“修改一个 URL”,而是一次跨多个层次的服务身份迁移。

涉及的主要层次包括:

DNS
TLS
Reverse Proxy
Apache VirtualHost
WordPress Rewrite
home/siteurl
Database URL
Serialized Data
GUID
Cookie
Cache
Permalink
旧 URL 策略
Rollback

比较稳妥的实践原则可以总结为:

  1. WordPress 物理目录能不动就不动。
  2. 先建立新域名链路,再切换 WordPress。
  3. 数据库替换必须支持 serialized data。
  4. GUID 不作为普通 URL 处理。
  5. 域名迁移和 permalink 重构分开做。
  6. 缓存清理属于迁移流程的一部分。
  7. 旧 URL 要明确选择 301 或彻底退役。
  8. 生产配置修改前必须建立可验证的回退点。
  9. 应用自己的备份或恢复文件不能仅根据文件后缀判断性质。
  10. 最终人工验收完成以前,不急于删除迁移备份。

一个真正完成的迁移,不只是:

新域名能打开

而应该达到:

新域名稳定工作
登录正常
后台正常
数据库 URL 正确
Serialized Data 正常
GUID 保持正确
静态资源正常
Feed/REST 正常
Cookie 正常
缓存已更新
旧地址行为明确
随时能够回退

达到这些条件以后,WordPress 从子目录到独立子域名的迁移才算真正完成。

Leave a Reply

Your email address will not be published. Required fields are marked *