zhChinese ▾
frFrench
deGerman
enEnglish
esSpanish
hiHindi
itItalian
jaJapanese
koKorean
noNorwegian
首页 最新 新闻 教程 消费 文化 热门视频 其他

Slug 中的重音符号与 ñ:文章为何返回 404

Publié le 06 novembre 2026
开发者双手在笔记本电脑键盘上编写代码,前方屏幕显示程序代码

文章已经发布,标题也没有问题,但公开链接却返回 404。PHP 错误日志和数据库都没有异常,文章确实存在。罪魁祸首只是一个字母:« La Niña » 中的 ñ。2026 年 11 月初,我们的博客就遇到了这个问题,值得逐层分析原因。

原理很简单:文章地址由标题生成,称为 slug(别名)。如果生成函数保留了 Web 服务器无法路由的字符,即使其他部分看起来正常,文章也会无法访问。

问题场景:slug 中残留 ñ

我们的别名生成函数使用手写替换表,逐个转换重音字符:é 变成 e、à 变成 a、ç 变成 c,依此类推。这张表覆盖了法语,却没有覆盖西班牙语和葡萄牙语,ñ、í、ó、ú、ò、ö、ã 都被保留下来。因此,标题 « La Niña : le risque économique caché derrière El Niño » 生成的别名仍含有 ñ。

服务器通过 .htaccess 文件中的类似规则路由文章:

RewriteRule ^article/([a-zA-Z0-9-_]+)$ /index.php?page_theme=article&alias=$1 [L]

字符类 [a-zA-Z0-9-_] 只接受 ASCII 字母、数字、连字符和下划线。浏览器将 ñ 编码为 %C3%B1 发送,Apache 在匹配前将其解码。得到的 UTF-8 字节显然不属于该字符类。规则无法匹配,也没有其他规则接管,最终返回 404 页面。

让问题更加明显的细节:结尾锚点

几天前,我们在模式末尾添加了 $。没有它时,规则只需要匹配地址开头:对于 /article/la-niña-…,它只捕获 la-ni,然后悄悄截断剩余部分。根据 CMS 解析部分别名的方式,结果可能是错误页面、被 Google 视为重复的页面,或者令人困惑的 404。结尾锚点让错误直接暴露,但并未消除根源:必须清理 slug。

广告

为什么 strtolower 无法解决问题

部分替换规则只处理小写字母,因为我们以为此前已经完成小写转换。但 strtolower() 仅处理 ASCII。从 PHP 8.2 开始,它甚至不再依赖区域设置,因此 Ñ 和 É 会保持不变。像 « EL NIÑO » 这样的全大写标题,即使修复小写替换表,也仍会留下重音字符。必须显式处理大写字母,或使用 mb_strtolower($titre, 'UTF-8')。

UTF-8、latin1 与失效的字符替换

手写替换表还存在另外两个陷阱。

  • 双重编码。 如果数据库使用 UTF-8,而标题经过 latin1 的 MySQL 连接传输(或反过来),ñ 可能变成 ñ。此时 str_replace('ñ', 'n', …) 找不到目标,slug 中便残留乱码。怀疑函数之前,先检查连接字符集,确保各处使用 utf8mb4。
  • 缺少 u 修饰符的正则表达式。 preg_replace('/[àâä]/', 'a', $s) 按字节工作:字符类包含六个字节,因此占两个字节的 « à » 会变成 « aa »。使用 /[àâä]/u 后,PCRE 才按字符处理,结果也就正确了。

如何在 PHP 中正确生成 slug

与其无限扩充替换表,不如先使用 Unicode 转写,再通过严格的允许列表过滤。

可靠方案:intl 扩展

function slugifier(string $titre): string
{
    // Translittère toute écriture en latin, puis en ASCII, et passe en minuscules
    $s = transliterator_transliterate('Any-Latin; Latin-ASCII; Lower()', $titre);
    // Liste blanche : tout ce qui n’est pas a-z ou 0-9 devient un tiret
    $s = preg_replace('/[^a-z0-9]+/', '-', $s);
    return trim($s, '-');
}

该方案能处理 ñ、转换成 ss 的 ß、œ、ø、ł,甚至能通过 Any-Latin 将非拉丁文字罗马化,而且不依赖服务器区域设置。

其他方案及其局限

  • iconv('UTF-8', 'ASCII//TRANSLIT', $s) 依赖当前区域设置。若没有 setlocale(LC_CTYPE, 'fr_FR.UTF-8'),glibc 可能输出 « Ni?a »;在 Alpine(musl)上结果更加不可靠。
  • Normalizer::normalize($s, Normalizer::FORM_D) 后接 preg_replace('/p{Mn}+/u', '', $s) 可以移除重音符号,但 œ、æ、ß、ø 并不是 Unicode 意义上的带重音字母,因此仍会保留。
  • 如果无法使用 intl 扩展,也可以保留手写替换表,但最后必须应用相同的允许列表,才能保证结果符合要求。

核心原则:slug 与重写规则必须使用相同的字符标准

问题根源在于我们对“允许的字符”有两套定义:PHP 函数中的隐含定义,以及 .htaccess 中的明确规定。应牢记:slug 生成器的允许列表必须与重写规则的字符类一致。 如果 .htaccess 接受 [a-z0-9-],生成函数就必须以 preg_replace('/[^a-z0-9]+/', '-', …) 结束,不能有例外。

还有一个建议:把连字符放在字符类最后,例如 [a-zA-Z0-9_-]。放在中间虽然可能被引擎容忍,但容易被理解为范围,迟早会造成麻烦。

预防胜于补救

  • 使用容易出问题的标题测试:« La Niña »、« EL NIÑO »、« Ça coûte 5 € »、« Œuvre complète »、« Straße »,以及一个日语标题。每个 slug 都必须匹配 Apache 规则。
  • 空 slug 的备用方案:标题如果只有符号,生成结果就是空字符串,此时应使用文章 ID。
  • 检查已有别名:使用 SELECT alias FROM articles WHERE alias NOT REGEXP '^[a-z0-9-]+$' 之类的查询,可以迅速找出已经损坏的地址。
  • 发布后验证:向公开 URL 发送简单的 HTTP 请求,确认返回 200,就能在上线时立即发现问题。

对 SEO 有什么影响?

Google 可以索引含有 Unicode 字符的 URL,因此地址中的 ñ 本身并不违规。但链接复制到消息或社交网络后,常常会显示成包含 %C3%B1 的编码形式,不够直观。只要生成函数能够严格过滤字符,简短易读的 ASCII slug 仍是最稳妥的选择。

我们最终采取了三个步骤:补全大小写字符的转写,在数据库中修正受影响文章的别名,并保留 .htaccess 中的结尾锚点,让未来的错误明确暴露,而不是被悄悄截断。

Tags
slug
URL
重音符号
htaccess
UTF-8
SEO
Envoyer à un ami
Signaler cet article
A propos de l'auteur