記事のURLを整理したい、サイトをHTTPSに移行したい、ドメインやサブドメインを変えたい。Webサイトを運用していると、URLを変える場面は意外と多い。そのとき必ず出てくるのが「リダイレクト」で、さらに「301にすべきか、302にすべきか」という選択がついてくる。
HTTPステータスコード全体の意味は「HTTPステータスコードの基礎」で整理した。今回はその中の 3xx、特に301と302に絞り、検索エンジンがそれぞれをどう受け取るか、そしてWordPressが裏でどんなリダイレクトを自動的に出しているかを、このブログ自身に curl を当てた実測結果を材料に解説する。
リダイレクトとは「このURLではなく、あちらを見てほしい」という返事
ブラウザやクローラーがあるURLにアクセスしたとき、サーバーは本文の代わりに「別のURLを見てほしい」と返すことがある。これがリダイレクトで、レスポンスは次の2つの要素でできている。
- ステータスコード(301・302など): 移動の「性質」を伝える
Locationヘッダー: 移動先のURLを伝える
HTTP/2 301
location: https://wpmm.jp/blog/
ブラウザはこれを受け取ると、自動的に Location のURLへアクセスし直す。訪問者から見ると、どちらのコードでも「別のページに飛んだ」という結果は同じである。違いが出るのは、その移動を一時的なものと見るか、恒久的なものと見るかという解釈の部分だ。
3xx の主な4種類
リダイレクトに使われる代表的なステータスコードは4つある。
| コード | 名前 | 性質 | 再送時のメソッド |
|---|---|---|---|
| 301 | Moved Permanently | 恒久的な移動 | POST が GET に変わることがある |
| 302 | Found | 一時的な移動 | POST が GET に変わることがある |
| 307 | Temporary Redirect | 一時的な移動 | 元のメソッドのまま |
| 308 | Permanent Redirect | 恒久的な移動 | 元のメソッドのまま |
301と302は古くからあるコードで、歴史的な経緯から、ブラウザはフォーム送信(POST)を受けたあとのリダイレクト先に GET でアクセスし直すことがある。307と308は、その曖昧さをなくすために後から定義されたもので、メソッドを変えずに再送することが仕様上決まっている。
通常のページのURL変更(訪問者が GET で見に来るもの)であれば、恒久的なら301、一時的なら302 で考えれば実用上は十分である。フォームの送信先や API のエンドポイントを移す場合は、308・307 を検討する余地がある。
検索エンジンは301と302をどう扱うか
ここが今回の本題である。「301なら評価が引き継がれ、302だと引き継がれない」という説明を見かけることがあるが、現在の Google の説明はもう少し正確だ。
Google は、リダイレクトを正規URL(canonical)を決めるためのシグナルとして扱っている。
- 301・308(恒久的): 「移動先のURLを正規として扱ってほしい」という強いシグナル。検索結果に表示されるURLは、移動先に置き換わっていく
- 302・307(一時的): 「移動先は一時的なもの」という弱いシグナル。検索結果には、しばらく元のURLが残りやすい
そして Google は、301・302 のどちらのリダイレクトでも、ページの評価(リンクの評価など)そのものが失われるわけではないと説明している。つまり違いの本質は「評価が消えるかどうか」ではなく、どちらのURLを検索結果に載せるべきかを、検索エンジンにどれだけはっきり伝えるかにある。
補足: 302のまま長期間放置したリダイレクトは、検索エンジン側が「実質的に恒久的な移動」と判断して扱いを切り替えることもある。ただし、それは検索エンジンの推測に任せた状態である。恒久的な移動だと分かっているなら、最初から301で伝えるほうが確実だ。
正規URLという考え方は、JP/EN の記事ペアを伝える hreflang と同じく「どのURLを代表として扱うか」に関わる話でもある。hreflang の仕組みは「hreflangタグの基礎」で解説した。
このブログで実際に起きているリダイレクト
理屈だけだと分かりにくいので、このブログに対して、さまざまな形のURLを curl で投げてみた。-D - でレスポンスヘッダーを表示し、ステータス・移動先・x-redirect-by ヘッダーを確認している。
curl -s -o /dev/null -D - https://www.wpmm.jp/blog/ | grep -i "^HTTP\|^location\|^x-redirect-by"
結果をまとめると次の通りである。
| アクセスしたURL | コード | 移動先 | 誰が出したか |
|---|---|---|---|
http://wpmm.jp/blog/ |
301 | https://wpmm.jp/blog/ |
Webサーバー(HTTPS化) |
https://www.wpmm.jp/blog/ |
301 | https://wpmm.jp/blog/ |
WordPress(x-redirect-by: WordPress) |
https://wpmm.jp/blog(末尾スラッシュなし) |
301 | https://wpmm.jp/blog/ |
Webサーバー(ディレクトリの補完) |
https://wpmm.jp/blog/<記事slug>(末尾スラッシュなし) |
301 | 末尾に / を付けたURL |
WordPress |
https://wpmm.jp/blog/?p=168 |
301 | その記事のパーマリンク | WordPress |
https://wpmm.jp/blog/http-status(存在しない途中までのslug) |
301 | /blog/http-status-codes-monitoring-basics/ |
WordPress |
https://wpmm.jp/en/blog/(旧EN URL) |
301 | https://en.wpmm.jp/blog/ |
.htaccess の書き換えルール |
https://wpmm.jp/blog/wp-admin/(未ログイン) |
302 | ログイン画面 | WordPress |
ここから読み取れることがいくつかある。
1つ目は、URLの「表記ゆれ」をまとめる移動はすべて301になっていること。http と https、www の有無、末尾スラッシュの有無、?p=ID 形式とパーマリンク形式。これらは同じ内容を指す別のURLであり、検索エンジンに「正規はこちら」と伝えたい恒久的な移動なので、301が適切である。
2つ目は、302が使われているのが管理画面だけであること。未ログインで管理画面を開くとログイン画面に移動するが、これは「ログインが済めば元のページに戻る」一時的な移動である。管理画面のURLが恒久的にログイン画面へ移ったわけではないので、302が正しい。
3つ目は、x-redirect-by ヘッダーで発生源が区別できること。WordPress は自分がリダイレクトを出すとき、既定でこのヘッダーに WordPress と付ける。ヘッダーがないものは、Webサーバーの設定や .htaccess の書き換えルール(「.htaccess の mod_rewrite ルールの読み方」で解説)など、WordPress より手前の層で処理されている。意図しないリダイレクトの原因を探すとき、最初に見るべき手がかりになる。
WordPressが自動で出しているリダイレクト
上の表のうち、WordPress が出しているものは、主にコアの3つの仕組みによる。このブログで動いている WordPress 7.1.2 のソースで確認した。
1. redirect_canonical() — 正規URLへの寄せ
template_redirect フックに登録されている関数で、アクセスされたURLと、WordPress が考える正規のURLが違えば301で寄せる。?p=168 → パーマリンク、末尾スラッシュの補完、www の有無の統一(サイトアドレスの設定に合わせる)などがこれにあたる。フックの仕組み自体は「WordPressのフック機構の基礎」で解説した通りで、プラグインがフィルターで挙動を変えることもできる。
2. wp_old_slug_redirect() — slugを変えた記事の旧URL
公開済みの記事の slug(URL末尾の文字列)を変更すると、WordPress は古い slug を _wp_old_slug というカスタムフィールドに自動で記録する。その後、古いURLにアクセスがあって 404 になりそうなときに、この記録から記事を探して301で新しいURLへ送る。
// wp-includes/query.php(WordPress 7.1.2)より抜粋
wp_redirect( $link, 301 ); // Permanent redirect.
ただし、記録されるのは公開状態の記事の slug を変えたときだけで、固定ページのような階層型の投稿タイプは対象外である。このブログでは公開後の slug 変更を禁止しているため、データベースを読み取り専用で確認すると _wp_old_slug は0件だった。
3. redirect_guess_404_permalink() — 404 になりそうなURLの推測
表の http-status の行が、この仕組みによるものだ。存在しない slug でアクセスされると、WordPress はそれを前方一致で含む記事を探し(post_name LIKE 'http-status%')、見つかればそこへ301で送る。URLの打ち間違いや途中で切れたリンクを救済するための仕組みである。
便利な一方で、注意点もある。
- 推測が外れることがある。前方一致なので、削除した記事のURLが、たまたま似た slug を持つ別の記事に送られることがある
- 「存在しないURLは404を返す」という前提で動作確認をしていると、結果が想定と食い違う
推測を完全一致に絞る strict_redirect_guess_404_permalink、推測自体を止める do_redirect_guess_404_permalink というフィルターが用意されているので、サイトの性質に応じて調整できる。
WordPressで自分でリダイレクトを書くときの落とし穴
テーマやプラグインのコードでリダイレクトを出すときは、wp_redirect() か wp_safe_redirect() を使う。ここでよくある落とし穴が、ステータスコードの既定値が302であることだ。
// wp-includes/pluggable.php(WordPress 7.1.2)
function wp_redirect( $location, $status = 302, $x_redirect_by = 'WordPress' ) {
つまり、URLの恒久的な移転のつもりで次のように書くと、意図に反して一時的な移動として伝わる。
// 302 になる(第2引数を省略)
wp_redirect( home_url( '/new-page/' ) );
exit;
// 恒久的な移動なら 301 を明示する
wp_safe_redirect( home_url( '/new-page/' ), 301 );
exit;
wp_safe_redirect() は、移動先を自サイト(と許可リストに登録したホスト)に限定する版である。移動先にリクエスト中の値が混ざる可能性があるなら、こちらを使う。また、どちらの関数もヘッダーを送るだけなので、直後に exit を書かないとそのまま後続の処理が走ってしまう点にも注意したい。
301が「強すぎる」ことによる落とし穴
301は検索エンジンにとって強いシグナルであるのと同時に、ブラウザにとっても「覚えておいてよい」移動である。301のレスポンスは、キャッシュに関するヘッダーがなくてもブラウザにキャッシュされうる。一度キャッシュされると、サーバー側で301の設定を外しても、そのブラウザは元のURLにアクセスせず、覚えている移動先へ直接向かってしまう。キャッシュの仕組みは「HTTPキャッシュヘッダーの基礎」を参照してほしい。
このため、次のような進め方が安全である。
- 試験中は302で設定し、動作を確かめてから301に切り替える
- 301に切り替えたあとで移動先を変える可能性があるなら、
Cache-Controlで有効期間を短く指定しておく - 動作確認はブラウザではなく
curlで行う(ブラウザのキャッシュに影響されない)
反対に、移動が本当に恒久的なら、302のまま放置しないことも大切だ。HTTPS化やドメイン変更を302で済ませてしまうと、検索結果に古いURLが長く残り続ける原因になる。
リダイレクトチェーンと移動先の整合性
リダイレクトが何段にも連なる状態をリダイレクトチェーンという。このブログでも、http://wpmm.jp/en/blog/ にアクセスすると次の2段を経由する。
curl -sL -o /dev/null -w "%{num_redirects} redirects -> %{url_effective} %{http_code}\n" \
http://wpmm.jp/en/blog/
# 2 redirects -> https://en.wpmm.jp/blog/ 200
1段目で https://wpmm.jp/en/blog/ に(HTTPS化)、2段目で https://en.wpmm.jp/blog/ に(サブドメインへの統合)移動している。2段程度なら実害はほとんどないが、サイトの移転を重ねるうちに3段、4段と増えていくことがある。Google のクローラーは一定の段数まではたどるが、段数が増えるほどクロールの負担が増え、途中で1つでも設定が壊れると最終的なページに届かなくなる。
チェーンを増やさないための原則は次の通りである。
- 新しいリダイレクトを追加するとき、古いリダイレクトの移動先も最終URLに書き換える(A→B→C ではなく、A→C と B→C にする)
- サイト内のリンク・
canonical・hreflang・サイトマップには、リダイレクトの移動先(最終URL)を書く。リダイレクトはあくまで外部からの古いリンクを救うためのもので、自サイトのリンクがリダイレクトを経由している状態は避けたい(サイトマップについては「robots.txtとsitemap.xmlの基礎」を参照) - ループに注意する。A→B、B→A のように循環すると、ブラウザは「リダイレクトが多すぎます」というエラーを表示する。HTTPS化の設定と WordPress のサイトアドレス設定が食い違っているときによく起きる
URLを変えるときの手順
最後に、記事やサイトのURLを変えるときの進め方をまとめる。
- そもそも変える必要があるかを考える: 公開済みのURLは、外部サイトからのリンク・SNSでの共有・ブックマークなどに広がっている。リダイレクトで救えるとはいえ、変えないのが最も安全である
- 旧URLと新URLの対応表を作る: 1対1で対応させる。内容の対応しない旧URLを、まとめてトップページに送るのは避ける。検索エンジンはこれを「中身のない404(ソフト404)」と見なすことがある
- 対応する新URLがないものは、404 または 410 を返す: 記事を削除して代わりになるページもないなら、無関係なページへ送るより「もう存在しない」と正直に返すほうが、訪問者にも検索エンジンにも誤解がない
- 恒久的な移動は301(または308)で設定する: 試験段階では302で動作を確かめ、問題がなければ301に切り替える
curlで1件ずつ確かめる: ステータス・Location・段数・最終的な200を確認する- サイト内のリンク・canonical・サイトマップを新URLに更新する
- リダイレクトは長く残す: Google はサイト移転の際、リダイレクトを少なくとも1年程度は維持するよう案内している。外部からのリンクは何年も残るので、外せる理由がなければ残し続けてよい
よくある落とし穴
wp_redirect()の第2引数を省略する: 既定値は302なので、恒久的な移転のつもりが一時的な移動として伝わる- いきなり301で設定して、移動先を間違える: ブラウザにキャッシュされ、サーバー側で直しても訪問者の環境で古い移動先に飛び続けることがある
- 削除した記事をすべてトップページへ送る: ソフト404と扱われやすく、訪問者にとっても探していたものが見つからない
- 404の推測リダイレクトを意識していない: 削除した記事のURLが、似た slug の別記事へ301で送られていることがある
- サイト内リンクを旧URLのままにする: リダイレクトを経由するぶんチェーンが伸び、クロールの手間が増える
- ブラウザで確認して「直った」と判断する: キャッシュの影響を受けるため、
curl -sIなどで直接ヘッダーを確かめる
まとめ
301は「恒久的に移動した」、302は「一時的に移動している」という意味の違いであり、検索エンジンにとってはどちらのURLを正規として扱うかのシグナルの強さの違いになる。301でも302でも評価そのものが失われるわけではないが、恒久的な移動なら301で明確に伝えるほうが、検索結果のURLは確実に新しいものへ置き換わっていく。
このブログに curl を当ててみると、HTTPS化・www の統一・末尾スラッシュ・?p=ID・旧EN URL といった「表記ゆれ」の統合はすべて301で、302は未ログイン時の管理画面からログイン画面への一時的な移動だけだった。WordPress は redirect_canonical()・wp_old_slug_redirect()・redirect_guess_404_permalink() によって多くの301を自動で出しており、発生源は x-redirect-by ヘッダーで見分けられる。
一方で、wp_redirect() の既定値が302であることや、301がブラウザにキャッシュされることなど、知らないと意図と違う結果になる点もある。URLを変えるときは、対応表を作り、試験は302で、確定したら301にし、curl で確かめ、サイト内のリンクを最終URLに揃える。この順番を守っておけば、URLの変更は怖いものではなくなる。