今天在一台 WordPress 上把固定链接从默认的 ?p=123 换成 /%postname%/。改完首页、关于页、文章列表页全都正常,唯独所有文章点进去是 404。
先分的第一个岔路:404 是谁发的
这台机器是 nginx 反代到容器里的 Apache,.htaccess 在,内容是标准那四行:
RewriteEngine On
RewriteBase /
RewriteRule ^index.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]
所以请求是进了 index.php 的。判断依据不是”我觉得”,是看响应体:nginx 自己的 404 一般是几十到一百多字节的纯文本或错误页;WordPress 的 404 是一整套模板,标题栏、站点名、样式表全在。把响应存下来 grep 一下 <title> 就能分辨,这一步能省掉后面一半时间。
再确认文章本身没丢
直接按 ID 走一次:/?p=6 返回 301,Location 指向 http://…/html-basics/。也就是说文章在、状态是 publish、canonical 也算得出来——只是 WordPress 自己算出来的那个地址打不开。这一条把排查范围从”数据”收缩到了”路由”。
把规则表导出来数
$ php -r 'require "/var/www/html/wp-load.php";
$r = get_option("rewrite_rules");
echo count($r), "n";
foreach ($r as $pat => $tgt) printf("%-45s => %sn", $pat, $tgt);'
98 条。关键几条长这样:
(?:category|tag)/([^/]+)/?$ => index.php?category_name=$matches[1]
([0-9]{4})/([0-9]{2})/([0-9]{2})/?$ => index.php?year=$matches[1]&monthnum=$matches[2]&day=$matches[3]
.?.+?/attachment/([^/]+)/?$ => index.php?attachment=$matches[1]
(.?.+?)(?:/([0-9]+))?/?$ => index.php?pagename=$matches[1]&page=$matches[2]
问题就在这儿:整张表里没有 ([^/]+)/?$ => index.php?name=$matches[1] 这一条。根路径上唯一能接住 /html-basics/ 的,是最后那条 pagename 规则。于是 WordPress 拿 html-basics 去 pages 表里找页面,找不到,404。
为什么会少这条规则
wp-includes/class-wp-rewrite.php 里有个开关叫 use_verbose_page_rules,init() 里(这台是 6.7 版,1942 行附近)有这么一句判断:固定链接结构如果以 %postname%、%category%、%tag%、%author% 开头,就把这个开关置 true。
置 true 之后,rewrite_rules() 拼接的顺序变了(1454 行附近),页面规则排到了文章规则前面——设计意图是”页面和文章可能同名,让页面优先”。至于根级那条文章规则为什么整条没进最终表、而不只是排在后面,我只核到「导出规则表、确认它不存在」这一步,再往下逐行扒 generate_rewrite_rules() 的分支我没做,所以这里不下结论。凭最终规则表这个事实,已经够修了。
修法:别让结构以通配符开头
把 /%postname%/ 换成 /post/%postname%/。结构以字面量 post 开头,verbose 开关回到 false,规则表里随即出现:
post/([^/]+)/?$ => index.php?name=$matches[1]
文章全部 200。代价是 URL 多了四个字符,旧链接全废(这个站刚建,没历史包袱)。
另外两条路,各自的账:
- 用日期结构
/%year%/%monthnum%/%postname%/:规则最稳,兼容性最好,但 URL 变长,而且改回去还得再刷一次。 - 挂
rewrite_rules_array过滤器手动插回那条规则:可行,但等于自己维护一份 WordPress 的路由表,升级、换插件都得重新确认,不划算。
一句提醒
flush_rewrite_rules() 是幂等的——你刷十遍都没用,因为规则本身就是按这个结构算出来的。看到”我刷过伪静态了还是 404″这种描述,别急着怀疑缓存插件,先把 rewrite_rules 这个 option 整张导出来看一眼,比猜快得多。