スラッグのアクセントとñが404を招く理由
記事を公開し、タイトルも正常なのに、公開リンクは404を返す。PHPのエラーログにもデータベースにも異常はなく、記事は存在する。原因は「La Niña」の ñ という一文字だった。2026年11月初めにこのブログで起きた事例を詳しく解説する。
仕組みは単純だ。記事のURLはタイトルから生成され、その末尾部分を slug(エイリアス)と呼ぶ。生成関数がWebサーバーでルーティングできない文字を残すと、記事が正常に存在していてもアクセスできなくなる。
原因:ñが残ったスラッグ
以前のエイリアス生成関数は手作業の置換表でアクセントを一文字ずつ変換していた。éは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の2バイトはこの文字クラスに含まれない。ルールは一致せず、代替ルールもなければ404ページになる。
さらに問題を明確にした末尾アンカー
数日前にパターンの末尾へ $ を追加していた。これがなければURLの先頭だけが一致するため、/article/la-niña-… では la-ni だけを取得し、残りを黙って切り捨てる。CMSが部分的なエイリアスをどう解決するかによって、別の記事、Googleから見た重複ページ、原因不明の404になる。アンカーで失敗は明確になるが、根本原因は解決しない。スラッグ自体を正しく生成する必要がある。
strtolowerだけでは解決しない
一部の置換処理は小文字だけを対象にし、事前の小文字化を前提としていた。しかし strtolower() が扱うのはASCIIだけだ。PHP 8.2以降はロケールにも依存せず、Ñ や É はそのまま残る。「EL NIÑO」のような大文字タイトルは、小文字用の修正後もアクセントが残った。大文字を明示的に処理するか、mb_strtolower($titre, 'UTF-8') を使おう。
UTF-8、latin1、置換失敗の落とし穴
独自の置換表には、さらに二つの落とし穴がある。
- 二重エンコード。 UTF-8のデータベースに対してlatin1のMySQL接続を経由した場合(またはその逆)、ñが
ñになることがある。str_replace('ñ', 'n', …)は一致せず、壊れた文字がスラッグに残る。関数を疑う前に接続文字セットを確認し、全体をutf8mb4に統一する。 u修飾子のない正規表現。preg_replace('/[àâä]/', 'a', $s)はバイト単位で処理する。文字クラスは6バイトを含むため、2バイトの「à」が「aa」になる。/[àâä]/uならPCREが文字単位で処理し、正しい結果になる。
PHPで正しいスラッグを生成する
置換表を際限なく増やすより、Unicodeの翻字を利用し、厳格な許可リストでフィルタリングするほうがよい。
堅牢な方法:intl拡張
function slugifier(string $titre): string
{
// すべての文字体系をラテン文字、ASCIIへ変換して小文字化
$s = transliterator_transliterate('Any-Latin; Latin-ASCII; Lower()', $titre);
// 許可リスト:a-zと0-9以外はハイフンに変換
$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拡張が使えない場合は独自の置換表でもよい。ただし最後に同じ許可リストでフィルタリングすることが必須だ。
重要なのはスラッグと書き換えルールの一致
根本的な問題は「許可される文字」の定義が二つあったことだ。PHP関数側の暗黙の定義と、.htaccess 側の明示的な定義である。原則は単純だ。スラッグ生成器の許可リストは書き換えルールの文字クラスと一致させる。.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」、そして日本語のタイトル。すべてのスラッグがApacheルールに一致する必要がある。
- 空のスラッグへの代替策:記号だけのタイトルは空文字列になる。その場合は記事IDを使用する。
- 既存エイリアスを点検する:
SELECT alias FROM articles WHERE alias NOT REGEXP '^[a-z0-9-]+$'のようなクエリで、壊れたURLをすぐに見つけられる。 - 公開後に確認する:公開URLへのHTTPリクエストが200を返すか調べれば、公開直後に問題を発見できたはずだ。
SEOへの影響は?
GoogleはUnicode文字を含むURLをインデックスできるため、URL内のñ自体は禁止されていない。ただしメッセージやSNSへコピーすると、%C3%B1 のような読みにくいエンコード表記になることがある。不要な文字を通さない生成関数を使い、短く読みやすいASCIIスラッグにするのが安全だ。
今回の修正は三つだけだった。大文字と小文字の翻字を補完し、データベース内の対象記事のエイリアスを修正し、.htaccess の末尾アンカーを維持した。これで今後の問題も、黙って切り捨てられず明確に現れる。
Japanese
French
German
English
Spanish
Hindi
Italian
Korean
Norwegian
Chinese


