同じ話題の記事を 1 本にまとめる:元の記事は下書きにして、URL をテーマの一覧で 301 転送する

機能追加
広告WordPress・PHP を学べる本

Amazonのアソシエイトとして、WordPress functions.phpは適格販売により収入を得ています。

管理している 2 つのサイトで、同じ話題の記事がいくつもできていました。

読む人から見ると、どれを読めばよいか分かりません。

検索でも、似た記事どうしで順位を取り合ってしまいます。

そこで、話題が重なった記事を 1 本にまとめ、元の記事の URL はまとめ先へ 301 で転送することにしました。

この記事では、その作り方と、確認した結果を書きます。

まとめたもの

サイト 元の記事 / まとめ先
サイト A(このブログ) 元の記事:1 本(WordPress のプラグインの選び方と All in One SEO)まとめ先:SEO のプラグインの選び方(書き直した記事)
サイト B(お客様のサイト) 元の記事:同じ季節の話題のコラム 6 本まとめ先:その話題のコラム 1 本(7 本分の内容をまとめた)

どちらも、元の記事は消さずに「下書き」にしました。

消さなければ、あとで内容を見直したり、戻したりできます。

やったことの流れ

順 やること / 理由
1 やること:まとめ先の記事に、元の記事のよいところを取り込んで書き直す理由:元の URL から来た人が、まとめ先で同じ答えを見つけられるように
2 やること:元の記事を下書きにする理由:消さずに残す。下書きの URL は、外からは 404 になる
3 やること:テーマに転送の一覧を足す理由:404 になった元の URL だけを、まとめ先へ 301 で送る
4 やること:curl で元の URL を見る理由:301 と転送先(Location)を確認する
5 やること:サイトマップと内部リンクを見る理由:元の URL が残っていないかを確認する

301 と 302 のちがい

転送には、ずっと移す「恒久的」なものと、しばらくだけの「一時的」なものがあります。

Google 検索セントラルの「リダイレクトと Google 検索」には、次のように書かれています。

種類 状態の番号 / 検索結果に出る URL / 正規の URL の手がかりになるか
恒久的 状態の番号:301・308検索結果に出る URL:転送先正規の URL の手がかりになるか:なる(転送先が正規版というシグナル)
一時的 状態の番号:302・307検索結果に出る URL:転送元正規の URL の手がかりになるか:ならない

同じページでは、URL を変えるときは、できるだけサーバー側の恒久的なリダイレクトを使うようにすすめています。

記事をまとめたら元に戻すつもりはないので、301 にしました。

なお、同じページには、転送を何か月残せばよいかという期間の目安は書かれていませんでした。

戻す予定がなければ恒久的なリダイレクトを使う、とだけ書かれています。

転送の一覧をテーマに持つ理由

転送は、プラグインでも .htaccess でもできます。

今回は、テーマのファイルに一覧を書く形にしました。

理由 内容
git に残る テーマは git で管理しているので、いつ・何を・どこへ転送したかが、コミットの記録に残る
プラグインを増やさない 転送が数本なら、数十行の PHP で足りる。プラグインの更新や設定の画面が増えない
404 のときだけ動かせる WordPress が 404 と決めたあとに動くので、公開中のページをまちがって転送しない
一覧が 1 か所にまとまる これからまとめる記事が増えても、一覧に 1 行足すだけ

コード:template_redirect で 404 のときだけ転送する

このブログでは、テーマの functions フォルダーに merged-redirects.php を足しました。

コメントは、記事用に短くしています。

functions/merged-redirects.php
<?php
// まとめた記事の URL へ 301 で転送する
// - WordPress が 404 と決めたときだけ動く(下書きにした記事は 404 になる)
// - 公開中のページには触らない
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * まとめた記事の転送の一覧(元のパス => 転送先のパス)
 */
function fs_merged_post_redirects() {
	return array(
		'/archives/1317' => '/archives/1319', // プラグインの選び方と All in One SEO → SEO のプラグインの選び方
	);
}

function fs_redirect_merged_posts() {
	if ( ! is_404() ) {
		return;
	}
	$path = (string) wp_parse_url( isset( $_SERVER['REQUEST_URI'] ) ? wp_unslash( $_SERVER['REQUEST_URI'] ) : '', PHP_URL_PATH );
	$path = untrailingslashit( $path );
	$map  = fs_merged_post_redirects();
	if ( isset( $map[ $path ] ) ) {
		wp_safe_redirect( home_url( $map[ $path ] ), 301 );
		exit;
	}
}
add_action( 'template_redirect', 'fs_redirect_merged_posts', 0 );
書き方 役目
template_redirect どのページを出すかが決まったあと、画面を出す前に動くフック。ここなら is_404() が使える
優先度 0 同じフックのほかの処理(ふつうは優先度 10)より先に動かす。理由は下に書きます
is_404() WordPress が「このページはない」と決めたときだけ true。下書きの記事の URL は、ログインしていない人には 404 になる
wp_parse_url( …, PHP_URL_PATH ) URL からパスだけを取り出す。?nc=… のようなクエリは外れる
wp_unslash() $_SERVER の値に付くことがあるバックスラッシュを外す
untrailingslashit() 最後の「/」を外す。/archives/1317 と /archives/1317/ を同じに見る
isset( $map[ $path ] ) 一覧にあるパスだけを転送する。ほかの 404 には触らない
wp_safe_redirect( …, 301 ) 301 で転送する。転送先が自分のサイトのときだけ送る安全な関数
home_url() サイトのアドレスを頭に付ける。作業環境でも本番でも、同じ一覧のまま使える
exit 転送のあとに、404 のページを出さないように止める

このファイルは、テーマの functions.php から読み込みます。

functions.php(足した 1 行)
require_once ( dirname(__FILE__) . '/functions/merged-redirects.php' );

サイト B の一覧

お客様のサイトも、同じ作りのファイル(functions/column-merge-redirect.php)にしました。

ちがうのは、一覧の内容だけです(関数の名前の頭は、記事では mysite_ にしています)。

functions/column-merge-redirect.php(一覧のところ)
function mysite_merged_column_redirects() {
	return array(
		'/archives/column/20260704' => '/archives/column/20260623', // (元の記事のタイトル)
		'/archives/column/20260703' => '/archives/column/20260623', // (元の記事のタイトル)
		'/archives/column/20260630' => '/archives/column/20260623', // (元の記事のタイトル)
		'/archives/column/20260627' => '/archives/column/20260623', // (元の記事のタイトル)
		'/archives/column/20260625' => '/archives/column/20260623', // (元の記事のタイトル)
		'/archives/column/20260624' => '/archives/column/20260623', // (元の記事のタイトル)
	);
}

6 本とも、まとめ先は同じ 1 本です。

コメントに元の記事のタイトルを書いておくと、あとで一覧を見たときに、何をまとめたかが分かります。

優先度を 0 にした理由

このブログには、もとから「404 のページをトップへ送る」処理がありました。

もとからあった処理(短くしたもの)
// もとからあった処理(優先度 10):404 のページをトップへ送る
function is404_redirect() {
	if ( is_404() ) {
		// (amp の付いた URL の転送は省略)
		wp_safe_redirect( home_url( '/' ) ); // 302 リダイレクト
		exit();
	}
}
add_action( 'template_redirect', 'is404_redirect' );

この処理は優先度 10 で、302 でトップへ送ります。

転送の処理を同じ優先度 10 にすると、読み込む順によっては、元の記事の URL がトップへ 302 で送られてしまいます。

そこで、転送の処理を優先度 0 にして、先に動くようにしました。

一覧にない 404 は、今までどおりトップへ送られます(下の「確認した結果」)。

クライアントのサイトでも、消した記事の URL に 410 を返す処理を同じ形(template_redirect・優先度 0・404 のときだけ)で設定していたので、それに合わせました。

確認した結果

作業環境と本番で、元の URL を curl -I で見ました。

キャッシュを避けるため、URL に ?nc=(乱数) を付けています。

作業環境のサイト A(例)
$ curl -I "http://example.local/archives/1317?nc=(乱数)"
HTTP/1.1 301 Moved Permanently
X-Redirect-By: WordPress
Location: http://example.local/archives/1319

サイト A

URL 作業環境 / 本番
元の記事 /archives/1317 作業環境:301 → /archives/1319本番:301 → /archives/1319
元の記事(最後に /)/archives/1317/ 作業環境:301 → /archives/1319本番:試していません
まとめ先 /archives/1319 作業環境:200本番:200
一覧にない、ない記事の URL 作業環境:302 → トップ(もとからの処理)本番:試していません

どちらも、返事に X-Redirect-By: WordPress が付いていて、WordPress の中で転送していることが分かりました。

転送先には ?nc=… が付いていませんでした。

パスだけを見て、一覧の転送先へそのまま送る作りなので、クエリは引き継ぎません。

サイト B(お客様のサイト)

本番では、元の 6 本はもう下書きになっています。

URL 本番
元のコラム 6 本(/archives/column/20260624 ほか) 6 本とも 301 → /archives/column/20260623
まとめ先 /archives/column/20260623 200

作業環境の DB は、まとめる前に本番から複製したものなので、元の 6 本がまだ公開のままでした。

そのままでは 6 本とも 200 で、転送されませんでした。

公開中のページには触らない、という作りのとおりです。

そこで、作業環境の DB で 6 本の状態をメモしてから下書きにして、もう一度見ました。

URL 作業環境(公開のまま) / 作業環境(下書きにしたあと)
元のコラム 6 本 作業環境(公開のまま):6 本とも 200作業環境(下書きにしたあと):6 本とも 301 → /archives/column/20260623
元のコラム(最後に /)1 本 作業環境(公開のまま):試していません作業環境(下書きにしたあと):301 → /archives/column/20260623
まとめ先 作業環境(公開のまま):200作業環境(下書きにしたあと):200
一覧にない、ない URL 作業環境(公開のまま):404作業環境(下書きにしたあと):試していません

見たあと、6 本はメモのとおり公開に戻しました。

内部リンクとサイトマップ

転送があっても、サイトの中のリンクが元の URL のままだと、毎回 1 回よけいに転送されます。

作業環境の DB で、公開中の記事の本文とカスタムフィールドに、元の URL が入っていないかを数えました。

サイト 探した URL / 本文 / カスタムフィールド
サイト A 探した URL:archives/1317本文:0カスタムフィールド:0
サイト B 探した URL:元のコラム 6 本の URL本文:6 本とも 0カスタムフィールド:6 本とも 0

本番では、トップ・コラムの一覧・まとめ先の記事のページを開いて、元の URL へのリンクがないこと(0 件)を見ました。

ほかのページは、本番では全部は見ていません。

サイトマップは、SEO のプラグイン(All in One SEO)が作っています。

サイト 見たサイトマップ / URL の数 / 元の URL / まとめ先
サイト A 見たサイトマップ:投稿のサイトマップURL の数:192元の URL:なしまとめ先:あり
サイト B 見たサイトマップ:コラムのサイトマップURL の数:58元の URL:6 本ともなしまとめ先:あり

下書きにした記事は、サイトマップから外れていました。

まとめ先の記事の canonical(正規の URL)も、まとめ先の URL そのものになっていました。

気をつけること

  • 元の記事を「ゴミ箱」に移動したときの動きは試していません。今回は、内容を残すために下書きにしました
  • クエリ(? のあと)は引き継ぎません。広告の計測の値などを付けた URL を転送したいときは、作りを変える必要があります
  • 転送が何百本にもなるときは、テーマの一覧より、プラグインやサーバーの設定のほうが向いているかもしれません(試していません)

まとめ

本番で試していない、最後に「/」を付けた元の URL と、一覧にない URL を、本番でも curl -I で見ます。本番でまだ開いていないページに、元の URL へのリンクが残っていないかも確認します。

参考

SiteBeacon(サイトビーコン)

サイトが止まったら、すぐに気づける。

ホームページを 5 分ごとにチェックして、止まったとき・エラーが出たとき・SSL やドメインの期限が近いときに、メールでお知らせします。2 サイトまでは、ずっと無料で使えます。

SiteBeacon を見る サイトを直す相談をする

監視で気づいたあとの直す作業も、同じ担当がお受けします。