本文へスキップ
サーバーと .htaccess

WordPress の .htaccess リダイレクト設定:301 を正しく書く

順位を落とさずに URL を移す方法。使うべき WordPress の .htaccess リダイレクトディレクティブ、記述位置、そしてロックアウトせずに検証する手順を解説します。

公開

URL を移しても旧 URL の評価を保ちたい。そのために必要なのは .htaccess の 301 です。ただし WordPress に消されない位置に置き、適切なディレクティブで書き、WordPress が実際に返す URL をぴったり指し示す必要があります。この三つのどれかを外すと、リダイレクトの連鎖か、ループか、wp-admin から締め出される 500 が待っています。

判断、位置、検証の順に見ていきます。

どのディレクティブか:Redirect、RedirectMatch、RewriteRule

関係する Apache モジュールは二つあり、互換性はありません。

Redirect(mod_alias) — 既知のパス一つを一つの転送先へ。最も単純に済む方法です。

Redirect 301 /old-page/ https://example.com/new-page/

覚えておくべき挙動が二つあります。完全一致ではなくパスの前方一致なので、/old-page//old-page/anything/ にも一致し、転送先に /anything/ を付け足します。またクエリ文字列は自動的に引き継がれます。

RedirectMatch(mod_alias) — 一つの正規表現で多数の URL をカバーします。セクションごと移すときはこれです。

RedirectMatch 301 ^/blog/(.*)$ https://example.com/articles/$1

キャプチャグループ (.*) が転送先の $1 になるため、/blog/hello-world//articles/hello-world/ に着地します。先頭のスラッシュに注意してください。mod_alias のパターンは URL パス全体に一致します。

RewriteRule(mod_rewrite) — リダイレクトが条件に依存する場合に必要です。ホスト名、プロトコル、クエリ文字列、ユーザーエージェントなど。mod_alias にはこれらを判定する手段がありません。

RewriteEngine On
RewriteCond %{HTTP_HOST} ^example\.com$ [NC]
RewriteRule ^(.*)$ https://www.example.com/$1 [R=301,L]

ディレクトリ単位の .htaccess では、パターンから先頭のスラッシュが取り除かれます。ここが ^(.*)$ で、mod_alias の例が ^/blog/ である理由はそれです。この違いの取り違えこそ、コピーしてきたルールが何の反応も示さない最大の原因です。

既定では mod_alias を選んでください。RewriteRule に手を伸ばすのは条件判定が要るときだけ。正規表現が減れば、間違えようも減ります。

ルールを置くべき場所

独自のリダイレクトは # BEGIN WordPress の行よりに置きます。内側ではありません。

# --- custom redirects ---
Redirect 301 /old-page/ https://example.com/new-page/
RedirectMatch 301 ^/blog/(.*)$ https://example.com/articles/$1

# BEGIN WordPress
# The directives (lines) between "BEGIN WordPress" and "END WordPress" are
# dynamically generated, and should only be modified via WordPress filters.
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
RewriteBase /
RewriteRule ^index\.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]
</IfModule>
# END WordPress

独立した理由が二つあります。

  1. WordPress は自分のブロックを書き換えます。 設定 → パーマリンクを保存すると flush_rewrite_rules() が呼ばれ、マーカー間の内容をすべて破棄して生成し直します。内側に置いたルールは、次に誰かがその画面に触れた時点で消えます。数か月後かもしれず、その二つの出来事を結びつける人は誰もいません。
  2. 勝敗を決めるのは順序です。 WordPress のブロックは、ファイルでもディレクトリでもないリクエストをすべて index.php へ送る包括ルールで終わります。その後ろに置いた RewriteRule は決して実行されません。

正直に言うと、ややこしい点が一つあります。mod_alias と mod_rewrite は実際にはファイルの記述順どおりには動きません。Apache は URL 変換フェーズで mod_alias を処理し、ディレクトリ単位の mod_rewrite はもっと後の fixup フェーズで処理します。そのため、ファイル上は下にある Redirect 行が、上にある RewriteRule に勝つことがあります。重なり合うパスで両方が必要になったら、そのパスではどちらか一方のモジュールに統一してください。mod_alias 対 mod_rewrite の争いをデバッグするのは、1 時間を費やす価値がありません。

末尾スラッシュ

WordPress 自身の正規化リダイレクトは、ほとんどのパーマリンクに末尾スラッシュを付けます。つまりこう書くと、

Redirect 301 /old-page/ https://example.com/new-page

転送が 2 回になります。まず自分の 301 でスラッシュなしの URL へ、次に WordPress 自身の 301 でスラッシュが付きます。連鎖しても評価は引き継がれますが、クロール予算を浪費し、訪問者ごとに往復が一つ増えます。

まず転送先をブラウザーで開き、アドレスバーで落ち着いた URL をそのままコピーして使ってください。パーマリンク構造が .html で終わる、あるいは末尾スラッシュを付けない設定なら、それに合わせます。ここに万人共通の正解はなく、あるのは「WordPress が返すものに合わせる」だけです。

自分を締め出さずに検証する

.htaccess はリクエストのたびに読み込まれます。構文エラーは wp-admin を含むサイト全体に 500 を返すため、復旧経路は必要になる前に用意しておかなければなりません。

まずファイルをバックアップします。 SSH ならこうです。

cp /path/to/wordpress/.htaccess /path/to/wordpress/.htaccess.bak

サイトが 500 になったら、壊れたファイルに控えを上書きし直せばすぐ復旧します。別ウィンドウで SFTP のセッションを開いたままにしておいてください。サイトが落ちている最中にログインからやり直すところで、パニックが始まります。なお apachectl configtest.htaccess解析しません。サイトが死んでいても設定は正常だと報告します。

ブラウザーではなく curl で検証します。 ブラウザーは 301 レスポンスを強くキャッシュし、平然と昨日の結果を見せてきます。

curl -sI https://example.com/old-page/ | head -n 5

読むのは二点です。ステータス行が 301 であること、Location: ヘッダーが最終 URL と完全一致していること。連鎖全体を見たいときは追跡します。

curl -sIL https://example.com/old-page/ | grep -E '^(HTTP|Location)'

HTTP/1.1 301 の行が二つ以上あれば連鎖ができています。五つを超えたらループです。curl のほうが処理を打ち切って教えてくれます。

確信が持てないうちは 302 を使ってください。 302 は 301 ほど強くキャッシュされないため、間違えても数秒で取り返せます。curl が意図どおりの転送先を示したら 301 に切り替えます。コストはゼロで、この記事のどの習慣よりも多くのサイトを救ってきました。

ルールがまったく何も起きていないように見えるなら、そもそも Apache がそのファイルを読んでいるかを確認してください。ディレクトリに AllowOverride None が設定されていると Apache は .htaccess を完全に無視しますし、nginx はそもそも一切見ません。正規表現をデバッグする前に curl -IServer: ヘッダーを確認しましょう。お使いの Apache のバージョンとインストールパスに合った構文でブロックを組み立てるには、WordPress .htaccess ジェネレーターを使ってください。

そもそも .htaccess を使うべきでない場合

編集担当者が自分で管理する必要のある少数のリダイレクトなら、ルールをデータベースに保存するリダイレクト系プラグインのほうが適した道具です。ホスティングの移転を越えて生き残り、SSH も要りません。ただし代償は現実にあります。リダイレクトを返すために PHP を起動する必要があり、Apache が直接応答するより遅くなります。

.htaccess は構造的で恒久的な移動に使ってください。ドメイン変更、セクション名の変更、https や www の強制などです。単発の編集上のリダイレクトにはプラグインを使います。どの URL をどちらの層が所有しているかを把握しているなら併用しても構いません。二か所で定義されたリダイレクトは、悪い午後を待っているバグだからです。

FAQ

よくある質問

WordPress の .htaccess で独自のリダイレクトはどこに書きますか?

# BEGIN WordPress の行より上に書きます。マーカーの内側は絶対に避けてください。パーマリンク設定画面を誰かが保存するたびに、WordPress はマーカー内をすべて生成し直すため、内側に置いたルールは警告もなく消えます。順序の面でも位置は重要です。WordPress のブロックは、一致しなかったリクエストを index.php へ送る包括ルールで終わっているためです。

301 には Redirect、RedirectMatch、RewriteRule のどれを使うべきですか?

単一の既知のパスなら Redirect、一つのパターンで多数の URL をまとめるなら RedirectMatch、ホスト名・プロトコル・クエリ文字列といった条件に依存するなら RewriteRule を使います。Redirect と RedirectMatch は mod_alias 由来で、より単純です。RewriteRule は mod_rewrite 由来で、条件を判定できる唯一の手段です。

.htaccess のリダイレクトでループが起きるのはなぜですか?

たいていは転送先がそのルールに再び一致してしまい、同じルールが延々と発火するためです。もう一つよくある原因は、ロードバランサーや CDN の背後で https を強制している場合です。訪問者はすでに https を使っていても、サーバー側はすべてのリクエストを平文の http として認識します。この場合は X-Forwarded-Proto ヘッダーを判定してください。

WordPress のリダイレクトで末尾スラッシュは重要ですか?

重要です。WordPress の正規化リダイレクトはほとんどのパーマリンクに末尾スラッシュを付けるため、スラッシュなしの URL へ 301 を向けると 1 回で済むところが 2 回の転送になります。連鎖したリダイレクトでも評価は引き継がれますが、クロール予算を浪費し、訪問者を待たせます。スラッシュも含め、WordPress が実際に返す最終 URL と完全に一致させてください。

サイトを壊さずに .htaccess のリダイレクトを検証するには?

ブラウザーを信用する前に、ヘッダーのみを取得するオプションを付けた curl を旧 URL に対して実行し、ステータスコードと Location ヘッダーを読んでください。ブラウザーは 301 レスポンスを強くキャッシュするため、古い結果を表示します。また、必ず先に動作中のファイルの控えを取ってください。.htaccess の構文エラーは wp-admin を含む全ページで 500 を返します。

リダイレクトを追加したらサイト全体が 500 エラーになりました。なぜですか?

.htaccess のディレクティブが一つでも壊れていると、そのディレクトリ配下のすべての URL が管理画面もろとも停止します。よくある原因は、閉じられていない IfModule、RewriteEngine On の記述漏れ、Apache 2.2 と 2.4 のアクセス制御構文が一つのファイルに混在していることです。最後に追加したブロックを削除して再読み込みし、原因を切り分けてください。