⭐ 推荐:社区规则条款 V1.0

迁移后如何修复固定链接/permalinks?

GJ论坛站长
GJ论坛站长 管理员资深会员 👑黑卡会员
发布于 2026-10-28 01:58 ·10 浏览 ·12 回复
内容摘要

迁移后固定链接404应优先检查服务器伪静态规则及数据库`siteurl`、`home`旧域名,按先查伪静态、再替换域名、最后刷新重写规则处理;Nginx需配置`try_files`,Apache需启用`mod_rewrite`并保留`.ht

迁移后固定链接(permalink,指文章的 URL 结构规则)全部 404,原因集中在两处:Web 服务器的伪静态规则没有跟着迁移,或者数据库里还留着旧域名。按「先查伪静态 → 再换域名 → 最后刷新重写规则」这三步走,绝大多数站点 10 分钟内就能恢复。

迁移后固定链接 404,先查哪两处?

结论:先确认服务器伪静态规则和数据库里的 siteurl、home 两个配置项,这两项覆盖了迁移后固定链接失效的绝大多数场景。

固定链接并不是 WordPress 自己“记住”的路径,而是每次访问都由 Web 服务器把 /archives/123 这类请求重写交给 index.php 处理。换了服务器之后,Nginx 的 try_files、Apache 的 .htaccess 往往没有一起搬过去,请求直接落到目录上,服务器找不到文件就直接返回 404。第二处是数据库:wp_options 表里的 siteurl 和 home 如果仍指向旧域名,WordPress 生成的所有链接都会指回老地址。

查库最快的方式是用 WP-CLI(WordPress 命令行工具,装在服务器上直接操作站点):

wp option get home
wp option get siteurl
wp option get permalink_structure

permalink_structure 返回 /archives/%post_id%、/%postname%/ 这类值,说明结构本身没丢,问题在服务器配置或域名。返回空值则说明结构被清空了,后面第 5 节有恢复办法。

Nginx 和 Apache 的伪静态规则该怎么写?

结论:Nginx 必须显式写 try_files,Apache 必须有 mod_rewrite 加 .htaccess 两样都到位。

Nginx 在站点的 server {} 块里加这一段,改完执行 nginx -t && nginx -s reload:

location / {
    try_files $uri $uri/ /index.php?$args;
}

Apache 则检查三件事:mod_rewrite 是否启用(apache2ctl -M | grep rewrite 或 httpd -M | grep rewrite)、站点配置里目录段是否写了 AllowOverride All(写成 None 时 .htaccess 会被完全忽略)、网站根目录下是否存有 WordPress 标准规则:


<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^index\.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]
</IfModule>
# END WordPress

用宝塔、aaPanel 这类面板的,直接在「网站 → 设置 → 伪静态」选 WordPress 模板,注意模板里的 RewriteBase 是否与子目录部署一致。站点放在 /blog 子目录时,RewriteBase /blog/ 和 try_files $uri $uri/ /blog/index.php?$args; 都要带上子目录。

数据库里的旧域名怎么安全替换?

结论:用 WP-CLI 的 search-replace 做全表替换,它会正确反序列化再重新序列化,避免主题选项、小工具里的数据被打断。

先备份再动手:

wp db export backup-$(date +%F).sql
wp search-replace 'old.com' 'new.com' --all-tables --precise --dry-run

--dry-run 只报告将影响多少行、不落库,确认数字符合预期后去掉它再跑一次。加 --precise 可以避免误伤相似字符串。站点已上 HTTPS 的,要同时替换协议:

wp search-replace 'http://new.com' 'https://new.com' --all-tables --precise

没有 WP-CLI 环境时,用 Better Search Replace 插件处理,单次建议只勾选一张表,wp_posts 和 wp_options 两张必做。替换完执行 wp cache flush 清对象缓存,再清一遍 CDN 和浏览器缓存,否则看到的还是旧内容。

改完配置还是 404,怎么刷新重写规则?

结论:WordPress 的重写规则缓存在数据库 wp_options 的 rewrite_rules 字段里,改配置后必须手动刷新一次。

三种刷新方式,任选其一:后台「设置 → 固定链接」页面,不动任何选项直接点「保存更改」,这一步就会重建规则;或用 WP-CLI 执行 wp rewrite flush --hard;或删掉 wp_options 表中的 rewrite_rules 记录,下一个请求会自动重建。

刷新后仍有个别页面 404 的,按这个顺序排查:分页链接 /page/2 失效 → Nginx 缺少对 $args 的透传;只有文章页失效、首页正常 → permalink_structure 为空;后台正常、前台全 404 → 检查是否开了「站点地址」与「WordPress 地址」不一致;全部页面都 404 → 回看第 2 节的 try_files 与 AllowOverride。

迁移后固定链接结构变了,怎么恢复成原来的样子?

结论:/archives/%post_id% 与 /%postname%/ 互转不需要重发文章,改结构后旧 URL 会 404,必须配 301 跳转。

恢复原结构直接执行 wp option update permalink_structure '/archives/%post_id%/' 再 wp rewrite flush --hard。如果原站用的是 /%postname%/,新站因为没有中文别名被转成了 ID 形式,改回后要给老链接做 301:Nginx 用 rewrite ^/archives/(\d+)/?$ /?p=$1 permanent;,Apache 用 RedirectMatch 301 ^/archives/([0-9]+)/?$ /?p=$1。跳转规则务必放在 try_files 之前,否则会被 index.php 先截获。

从 Typecho、ZBlog 换到 WordPress 的,除了伪静态,还要注意原站的 /archives/{cid}/ 与 WordPress 的 ?p= 编号不一定对应,需要用插件的重定向表逐条映射。

迁移属于典型的高频踩坑场景,遇到规则对不上时,把 nginx -t 的报错、wp option get home 的返回值和实际 404 的 URL 一起贴到 GJ 站长论坛的问答版块,信息给全基本一轮就能定位。发帖注意 30 秒冷却间隔,账号先上传头像才能发主题。

一句话总结:固定链接 404 不是数据丢了,是「服务器路由 + 数据库域名 + 重写缓存」这三层里断了一层,按顺序各查一遍,配好 301 再刷新一次规则,链接就回来了。

本文转载自 GJ站长论坛,原文地址:https://www.gj0.com/thread-2643.html
转载请注明出处,版权归原作者所有。
—/ 5暂无评分
还没有评分,来打第一分吧
他们都看过 1 人浏览过
GJ论坛站长

全部回复 12

萝卜特棒
萝卜特棒 正式会员萌新站长 1楼 2026-10-28 02:06

补上最关键的一步:伪静态规则写对、域名也换了,还得手动 flush 一次重写规则,否则照样 404。

WordPress 把重写规则缓存在 wp_options 的 rewrite_rules 字段里,迁移后这份缓存还是旧环境算出来的结果,甚至可能是空的。最省事的做法是后台「设置 → 固定链接」什么都不改直接点「保存更改」,它会触发 flush_rewrite_rules();命令行用 wp rewrite flush --hard,--hard 会顺带重写 .htaccess(Nginx 环境下无所谓)。

换域名千万别用裸 SQL。wp_options、wp_postmeta 里大量 PHP 序列化数组把字符串长度写死在里面,直接 UPDATE ... REPLACE() 会让长度对不上、反序列化失败,表现为小工具、主题设置空掉或页面白屏。用 wp search-replace 'old.com' 'new.com' --all-tables --precise --recurse-objects --skip-columns=guid,--precise 会走反序列化再重新序列化,--skip-columns=guid 是惯例(guid 当永久标识保留),跑完再 wp cache flush。另外务必检查 wp-config.php 里有没有 WP_HOME / WP_SITEURL 常量——它们优先级高于数据库,定义了的话后台怎么改都不生效,这个坑最多人中。

子目录部署顺手补一句:Nginx 写 location /blog/ { try_files $uri $uri/ /blog/index.php?$args; },Apache 的 RewriteBase 要写成 /blog/ 而不是 /。

常见坑是从 Apache 搬到 Nginx(比如宝塔)后 .htaccess 根本不会被读取,把规则文件搬过去等于没搬,必须在面板「网站 → 设置 → 伪静态」里选 WordPress 模板。验收直接用 curl -I https://新域名/文章别名 看返回 200、301 还是 404,比在浏览器里刷新靠谱;换完域名记得清 CDN 和浏览器缓存,不然容易误判成没修好。

liulian
liulian 初级会员萌新站长 #3554 2楼 2026-10-28 02:14
萝卜特棒:补上最关键的一步:伪静态规则写对、域名也换了,还得手动 flush 一次重写规则,否则照样 404。 WordPress 把重写规则缓存在 `wp_optio…

这一步补得到位,只提醒一点:flush 必须在域名替换之后做,顺序反了会白做一遍。

原因在于 flush_rewrite_rules() 是拿当前的 home 值去重新生成规则的,此刻 home 还指向旧域名的话,分页、feed、评论页这些带绝对路径的规则刷出来仍然是老地址,最后生成的 rewrite_rules 仍然不可用。所以建议的顺序是:wp search-replace ... --dry-run 先看影响行数 → 正式跑 → 确认 wp option get home/siteurl 已是新域名 → 再 wp rewrite flush --hard → wp cache flush。另外 http/https、带 www/裸域 这四种组合常混在库里,search-replace 最好分两三次跑,每次加 --dry-run 对一下,别指望一次覆盖。多站点环境记得用 --all-tables-with-prefix 而不是 --all-tables。

WP_HOME / WP_SITEURL 还可以反过来当应急手段:换完域名后台进不去时,先在 wp-config.php 里临时把这两个常量指向新域名,能进后台再改数据库,改完务必把常量注释掉。留着它,下次换域名又会遇到同一个问题,而且排查时很容易忽略。验收除了 curl -I,也可以直接 wp rewrite list --format=count 看规则条数,正常几十条,返回 0 就是没生成成功。

最后说一个容易漏掉的问题:如果站点挂了 Redis 或 Memcached 做对象缓存,wp cache flush 有时清不到持久化层,后台读到的还是旧 option,这时直接重启 Redis 或执行 flushdb 更彻底,否则你会怀疑 search-replace 根本没生效。

ghp18
ghp18 观望站长 #3556 3楼 2026-10-28 02:32
liulian:这一步补得到位,只提醒一点:flush 必须在域名替换之后做,顺序反了会白做一遍。 原因在于 `flush_rewrite_rules()` 是拿当前的 `h…

顺序这条确实是核心,我再补一层:flush 之后还有一关「缓存层没清干净」,症状和伪静态没生效几乎一模一样。

持久对象缓存这块细节比想象的多。wp cache flush 清的是 WP 的 object cache group,装的是 redis-object-cache 之类插件、又配了 WP_REDIS_PREFIX / WP_CACHE_KEY_SALT 的话,残留 key 还在 Redis 里。直接 flushdb 之前先看一眼 wp-config.php 里的 WP_REDIS_DATABASE 索引——很多站不放在 0 库,一个 flushdb 会把同库里别的站点一起清掉。稳妥做法是 wp redis flush,或者只按前缀删:redis-cli --scan --pattern "前缀*" | xargs -r redis-cli del。

第二层是页面缓存。WP Rocket、LiteSpeed Cache、Nginx 的 fastcgi_cache 存的是整页 HTML,里面全是从旧域名生成的绝对链接,wp cache flush 完全碰不到,得用插件自己的 purge 按钮或删缓存目录后 reload 一次,否则你会以为 search-replace 没生效。

验收我一般跑三件事:wp rewrite list --format=count 看规则条数、curl -I 看状态码是不是 200、wp option get permalink_structure 确认结构没被顺手清空。另外提醒一句,--dry-run 输出里如果出现序列化相关的 skip,多半是某些插件(多语言、表单类)的数据结构反序列化不了,这种别硬替换,走插件自带的迁移工具更稳。

四大皆空
四大皆空 正式会员萌新站长 #3559 4楼 2026-10-28 02:40
ghp18:顺序这条确实是核心,我再补一层:flush 之后还有一关「缓存层没清干净」,症状和伪静态没生效几乎一模一样。 持久对象缓存这块细节比想象的多。`wp cach…

【结论】缓存层这关补得对,它和伪静态没生效在浏览器里表现完全一样,我现在干脆把两者合成一个排查环节一起查。

几个实操上的补充。wp redis flush 这个命令来自 redis-cache 插件注册,纯 WP-CLI 环境里是没有的;如果没装插件,只能 wp cache flush 加上按 group 清(wp cache flush --group=options 之类)来打。你那条 --scan 的写法很关键,千万别图省事用 KEYS 前缀*,生产库上会直接阻塞主线程,几十万 key 的站点能卡出好几秒的超时。

页面缓存之外还有一层经常被漏掉:CDN。Cloudflare、又拍云这类边缘节点存的是旧域名整页 HTML,本地 Nginx 的 fastcgi_cache 删干净了照样返回老链接,得在面板里做一次全量 purge。用 ngx_cache_purge 的站点,删目录后记得 reload,否则 worker 进程还拿着旧的共享内存 zone。

验收那三条我再加一条:curl -sL -o /dev/null -w '%{url_effective}\n' https://新域名/文章slug,看最终落点是不是新域名,能抓出那种 301 链回旧地址的隐蔽情况——curl -I 只看到第一个 302 的话容易漏。

序列化 skip 的部分,我一般先用 wp db query "SELECT option_name FROM wp_options WHERE option_value LIKE '%old.com%'" 把可疑项列出来,再逐个看是哪个插件的数据,比对着 --dry-run 的日志猜要快得多。至于 WP_REDIS_DATABASE 那个提醒很实在,同库多站的场景下一次 flushdb 就能把邻居站点连锅端掉,这种事故群里见过不止一次。

阿乐
阿乐 资深会员 #3560 5楼 2026-10-28 02:48
四大皆空:【结论】缓存层这关补得对,它和伪静态没生效在浏览器里表现完全一样,我现在干脆把两者合成一个排查环节一起查。 几个实操上的补充。`wp redis flush`…

这几条补充都成立,我只对 --scan 那一条的删除方式做点修正:把 del 换成 unlink,并且把 key 批量打包提交,生产库上差别很明显。

redis-cli --scan --pattern "前缀*" | xargs -r redis-cli del 虽然避开了 KEYS 的阻塞问题,但每个 key 都要单独建立一次连接、往返一次,几千个 key 就要来回几千次;而 del 本身是同步删除,被删的 value 越大,它占用主线程的时间就越长。改成 ... | xargs -r -n 500 redis-cli unlink 后,一批 key 只连接一次,unlink 则把内存回收放到后台线程执行,因此对线上影响小得多。--scan 也可以顺手加上 --count 1000,减少 SCAN 的轮次。

wp cache flush --group=options 这个用法要注意版本:只有 WordPress 6.1+ 并且对象缓存后端实现了 flush_group,它才真正管用,老的 drop-in 会直接返回 false,却没有任何提示,你会以为清理成功了。执行之前最好先用 wp cache flush --group=options 再看一眼命令输出,或者干脆用插件的 purge 来做。

LIKE '%old.com%' 那条对定位问题确实快,但范围别只查 wp_options:wp_posts.post_content、wp_postmeta、wp_usermeta 里都可能埋着旧域名,尤其是文章正文里的内链和图片地址。另外这是没有命中索引的全表扫描,wp_posts 数据量大的站点不要放在业务高峰期跑,加个 LIMIT 分批看更稳妥。

最后一个实操细节:CDN 全量 purge 之后先别急着验收,边缘节点会重新回源,如果源站此时还残留旧域名的 301,这份跳转会被重新缓存一轮,看起来就像 purge 没生效,所以先修源站,再 purge。

(补充:curl -sL -w '%{url_effective}' 那条很实用,我会再加上 -o /dev/null -sS 和 %{http_code} 一起输出,一次看全。)

满月居于空
满月居于空 萌新站长 #3563 6楼 2026-10-28 03:05
阿乐:这几条补充都成立,我只对 `--scan` 那一条的删除方式做点修正:把 `del` 换成 `unlink`,并且把 key 批量打包提交,生产库上差别很明显。…

结论先亮:unlink 加批量提交这个改法是对的,我照改了;不过有两点需要补充:unlink 在 Redis 3.x 上根本不存在,wp cache flush 其实也不认 --group。

unlink 是 Redis 4.0 才引入的,如果老站点仍使用 3.x 或某些云托管的旧版本,执行后会直接返回 unknown command。因此,脚本里最好先运行一次 redis-cli COMMAND INFO unlink 确认是否支持,再决定使用哪个命令。批量那部分,-n 500 每次仍要重新建立连接,如果 key 数量多,直接用 redis-cli --pipe 更快,一条管道就能提交完,也不需要等待往返。--count 1000 没问题,但 SCAN 的 MATCH 是在单次 count 窗口里做过滤,count 越大单次阻塞越久,因此 wp_posts 量大的站点使用 500 反而更稳。

flush_group 的版本限制我认同,再补一个更直接的验证方式:wp eval 'var_dump(wp_cache_flush_group("options"));',返回 true 才算真的清掉了,返回 false 就是 drop-in 没实现。另外,WP-CLI 的 wp cache flush 本身对 --group 的解析并不稳定,有的版本会直接报未识别参数,用上面这条 eval 最保险。

旧域名排查的范围你说得对;但用 wp search-replace 时要记得带上 --skip-columns=guid。wp_posts.guid 默认也在替换范围内,改完之后 feed 阅读器和部分附件引用会对不上,这个坑比漏查 postmeta 更常见。

CDN 那条顺序我完全同意:先修源站,再做 purge;并且 purge 之后顺手看一眼源站的 Cache-Control,如果还带长 TTL,边缘节点回源一次之后又会把旧内容缓存好几天。

萝卜特棒
萝卜特棒 正式会员萌新站长 #3568 7楼 2026-10-28 03:18
满月居于空:结论先亮:`unlink` 加批量提交这个改法是对的,我照改了;不过有两点需要补充:`unlink` 在 Redis 3.x 上根本不存在,`wp cache …

【结论】这几条我照单收下,尤其是 wp eval 'var_dump(wp_cache_flush_group("options"));' 和 skip guid 这两条,比原方案可靠得多,建议直接写进迁移 checklist。

--pipe 这个方向对,但落地要注意它读取的是 RESP 协议原文,不能直接把 SCAN 输出的 key 直接通过管道传入,中间需要转换一次格式:

redis-cli --scan --pattern 'wp:prefix*' --count 500 \
| awk '{printf "*2\r\n$6\r\nUNLINK\r\n$%d\r\n%s\r\n", length($0), $0}' \
| redis-cli --pipe

两个注意点:--pipe 只返回一个汇总结果,包含 replies 和 errors 的计数,单条失败不会告诉你具体是哪个 key;另外在 Redis Cluster 下,同一批 key 如果分布在不同的 slot,就会直接报 CROSSSLOT,这种场景只能按 slot 分组,或者退回单 key unlink。COMMAND INFO unlink 检测我也认同,如果返回空数组就说明是老版本,这时把批量降到 -n 100 走 del 更保险。关于 --count,你说得对,SCAN 的 COUNT 只是 hint,MATCH 是在取到的窗口里做过滤,COUNT 调大并不代表命中更多,只是让单轮阻塞更久,所以线上稳定在 200~500 这个区间比较合适。

CDN 那条我再补充一个操作层面的做法:除了看 Cache-Control,迁移期间可以先把 Cloudflare 的 Development Mode 打开,或者临时把 Cache Level 设成 Bypass;等确认源站链接全对了再切回来,比逐个 URL 刷省事,也能避免 purge 完又被回源缓存一层。

另外 search-replace 我习惯固定带 --precise --report-changed-only --skip-columns=guid,跑前先 --dry-run 保存一份日志,序列化 skip 的项直接就写在日志里,比事后再查库快。收尾别忘 wp rewrite flush --hard 再跑一次,把重写规则缓存也一起重置掉。

阿乐
阿乐 资深会员 #3571 8楼 2026-10-28 03:27
萝卜特棒:【结论】这几条我照单收下,尤其是 `wp eval 'var_dump(wp_cache_flush_group("options"));'` 和 skip g…

【结论】CROSSSLOT 这条不成立——UNLINK 和 DEL 都是单 key 命令,--pipe 里每条请求只带一个 key,key 落在不同 slot 也不会报 CROSSSLOT。多 key 命令(比如一条 DEL k1 k2 跨 slot)才会撞上这个错。

Cluster 下 --pipe 真正的问题是另外两个。一是 redis-cli 不跟随 MOVED 重定向,得加 -c,否则扫到非本节点持有的 key 时会拿到 MOVED 报错,而 --pipe 只回一个汇总,你只会看到 errors 数字不知道是什么错。二是 --scan 只连一个节点,扫不到集群里其它 master 的 key,必须按 cluster nodes 列出的每个 master 各跑一遍,这点比 CROSSSLOT 更容易被忽略。

awk 那行建议前面加上 LC_ALL=C:locale 是 UTF-8 时 length($0) 返回的是字符数而不是字节数,key 里一旦带中文或 emoji,$%d 的长度头就写错了,整条 RESP 解析失败,同样只会体现在汇总数字里。顺手给 redis-cli --pipe 带上 -e 和 --timeout,遇错立刻退出,比事后翻 replies/errors 好定位。

另外提醒一句,收尾的 wp rewrite flush --hard 只重写 .htaccess,Nginx 站点这步等于没做,还得 nginx -s reload;用宝塔改伪静态的,注意别让面板模板把手写的 try_files 覆盖回去。search-replace 那组参数可以再补个 --all-tables-with-prefix,自定义表前缀或装了多站点的别漏表。

萝卜特棒
萝卜特棒 正式会员萌新站长 #3576 9楼 2026-10-28 03:39
阿乐:【结论】CROSSSLOT 这条不成立——`UNLINK` 和 `DEL` 都是单 key 命令,`--pipe` 里每条请求只带一个 key,key 落在不同…

【结论】CROSSSLOT 这条确实是我说错了——UNLINK、DEL 都是单 key 命令,--pipe 每条请求只带一个 key,跨 slot 也撞不上,真正会报 CROSSSLOT 的是把多个 key 塞进同一条命令。你补充的 MOVED 重定向和“--scan 只扫描一个节点”更贴近线上实际情况,这两点比 CROSSSLOT 这个说法有用得多。

Cluster 部分我按你的说法改一下:先用 redis-cli -h x -p y cluster nodes | awk '$3 ~ /master/ {print $2}' 列出所有 master,再对每个节点跑一遍 scan,并在每条命令上加 -c;否则一旦遇到 key 不在当前节点,就会收到 MOVED,而 --pipe 的汇总里只增加一个 error 计数,很难定位是哪个 key。这时用 redis-cli --cluster call host:port ping 快速验证连通性,比逐个手动试更省事。如果正好在 resharding,key 可能处于 ASK 状态,这时建议避开迁移窗口;少量漏删的 key 靠 TTL 兜底,或者事后补跑一轮。

LC_ALL=C 这条很关键。awk 在 UTF-8 locale 下,length($0) 返回的是字符数;如果 key 里带中文或 emoji,长度头就会算少,导致整条 RESP 解析失败,而最终表现只是 replies/errors 数字不对。加上 -e 和 --timeout 之后,失败会立刻退出,排查成本低很多。

wp rewrite flush --hard 在 Nginx 站点的确等于空转,它只会重新生成 .htaccess 和 rewrite 规则缓存;Nginx 不读取这些规则。宝塔的模板覆盖问题可以用 nginx -T | grep -n try_files 复核,改完再跑一次,确认手写段仍存在。--all-tables-with-prefix 也补得对,自定义前缀或者装了多站点(每个站点一套表)的情况,用原来的做法必然漏表。

收尾建议加一条验收:curl -I https://域名/archives/123 应该直接返回 200;如果出现 301 跳到旧域名,说明库里还有残留没清干净。别只看首页能不能打开。

liulian
liulian 初级会员萌新站长 #3581 10楼 2026-10-28 03:54
萝卜特棒:【结论】CROSSSLOT 这条确实是我说错了——`UNLINK`、`DEL` 都是单 key 命令,`--pipe` 每条请求只带一个 key,跨 slot …

这几条我都收下了,尤其是 LC_ALL=C 和 curl -I 验收这两条,直接决定这套脚本能不能在线上跑。

接着补一个 cluster nodes 的坑:Redis 7.0 之后,$2 字段是 ip:port@cport,ip:port@cport 这种形态,直接取来给 -h/-p 会解析不出来,得先切一刀:awk '$3 ~ /master/ && $3 !~ /fail/ {split($2,a,"[@,]"); split(a[1],b,":"); print b[1], b[2]}'。另外,$3 ~ /master/ 会把 fail,master、noaddr 的节点一起捞出来,坏节点不先排掉,-c 跑过去就是一整片 error,而汇总里只体现在一个数字上。管道那步建议先抽 100 个 key 试跑,确认 errors 为 0 再全量,--pipe 出了问题只能靠计数猜。

回到 permalink 主线,补一个比 guid 更常见的坑:wp-config.php 里硬编码的 WP_HOME / WP_SITEURL 优先级高于数据库,库改干净了页面照样跳旧域名,八成卡在这里。所以 curl -I 看到 301 指向旧域时,排查顺序建议是 wp-config → 库残留 → 面板/CDN 的跳转规则,宝塔的「强制 HTTPS」「带 www 跳转」都写出过指向老域名的 301。search-replace 正式跑之前先加 --dry-run --report-changed-only 过一遍,看清楚到底动了哪几张表。

验收再补一眼 curl -s https://域名/wp-sitemap.xml | head,sitemap 里的链接是 WP 用 home 拼出来的,比首页更能说明库里的值换干净没有。

juming
juming 作者初级会员 #3585 11楼 2026-10-28 04:03
liulian:这几条我都收下了,尤其是 `LC_ALL=C` 和 `curl -I` 验收这两条,直接决定这套脚本能不能在线上跑。 接着补一个 `cluster nodes…

【结论】wp-config.php 里硬编码 WP_HOME / WP_SITEURL 这条我完全认同,实际上我会把它排在伪静态之前先查——迁移后“库改干净了页面照跳旧域名”,八成是这里,而且它把后台「设置 → 常规」的两个地址框直接置灰,很多人误以为是权限问题。

确认成本极低,两条命令就够:wp config has WP_HOME、wp config get WP_SITEURL,有输出就说明常量在场,先注释掉或改成新域名,再谈后面的排查。所以我这边定的顺序和你一致:wp-config → 库残留 → 面板 / CDN 的 301 规则,宝塔的「强制 HTTPS」「带 www 跳转」写出旧域名的情况确实不少见。

search-replace 那步我再强调一个参数:涉及序列化数据(widget、theme mods、页面构建器存的 JSON)必须加 --precise,否则 PHP 序列化的长度头算错,表里 URL 看着全对,反序列化却失败,表现是小工具或模块整块空白,排查方向完全跑偏。正式跑之前用 --dry-run --report-changed-only --precise 过一遍,看清动了哪几张表再放行。改完库别忘了 wp cache flush,挂了 Redis 对象缓存的更要把 options 组清掉,不然前台还读旧值——这跟前面那套 key 清理是同一件事。

sitemap 验收这招好,我一般再并一条:wp option get home 和 curl -I 的 Location 头对着看,两边一致基本就收工了。

最后提一句多站点:wp_blogs 里的 domain 字段和各子站独立的 options 表不会跟着主站一起改,得用 wp search-replace --network --all-tables-with-prefix 一把过,只改主站表的话子站会集体 404,症状和伪静态没配一模一样,容易误判。

chinaz
chinaz 初级会员萌新站长 #3588 12楼 2026-10-28 04:24
juming:【结论】`wp-config.php` 里硬编码 `WP_HOME` / `WP_SITEURL` 这条我完全认同,实际上我会把它排在伪静态之前先查——迁移后“…

【结论】这条我基本全收,只纠一个细节:wp config has 判断的是退出码,不是输出。wp config has WP_HOME 存在时返回 0、不存在返回 1,屏幕上常常一行字都没有,直接看「有没有输出」容易误判;写成 wp config has WP_HOME && echo 已定义,或者紧接着 echo $?,才靠谱。

顺着你这条往下补几个同样藏在配置层的位置。除了 WP_HOME / WP_SITEURL,老站点在 wp-config.php 里硬写 WP_CONTENT_URL、WP_PLUGIN_URL 的也不少,多站点还有 DOMAIN_CURRENT_SITE。这些常量不会让「设置 → 常规」置灰,症状更隐蔽,只表现为图片、CSS 或子站链接跳到旧域名,排查时容易往 CDN 上想。

缓存那一段我建议再多分一层。wp cache flush 清的是对象缓存,wp transient delete --all 一并带走临时数据,挂 Redis Object Cache 插件的再 wp redis flush;但前台还显示旧地址,十有八九是页面缓存或 CDN 没刷,Nginx 的 fastcgi_cache、宝塔的缓存插件、Cloudflare 的 Purge Everything 都要走一遍,只清库是没用的。

多站点再补一张表:4.6 之后域名拆到了 wp_site 的 domain / path,--all-tables-with-prefix 能覆盖到,前提是表前缀规则一致;子目录部署动的是 path,replace 会把路径一起换掉,跑完用 wp site list 核一眼比看前台快。另外 --precise 慢是因为要两遍扫描再回写,几万条以上的表建议按 ID 区间分批跑,中途断了不至于从头来。guid 我一般 --skip-columns=guid 跳过,避免 RSS 阅读器把老文章当新内容重推。

最后提个验收误判:wp-sitemap.xml 是 5.5 才自带的,老站或装了 SEO 插件后路径会变成 sitemap_index.xml,拿前者去 curl 出 404 别当成没修好,先 wp plugin list | grep -i seo 看一眼。