0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

レコードを統合して消したURLは、404にせず統合先へ308で転送する ─ 対応表・連鎖・SSRでの受け方

0
Posted at

この記事でできること

同じものを指す2件のレコードを1件にまとめ、片方を削除する処理は珍しくありません。重複して取り込んだ記事、二重登録された商品、統合したアカウントなどです。このとき、消した側の詳細ページのURLは404になります。

そのURLが検索結果やブックマーク、SNSの共有に残っていると、同じ内容が統合先に残っているのに、読者は「ページが見つかりません」にたどり着きます。統合先へ恒久的なリダイレクト(308)で転送すれば、読者は同じ内容を読めますし、検索エンジンにもURLの移転として伝わります。

この記事は、個人開発している技術記事キュレーションサービスDevPickで、重複記事の統合で消えた旧URLを308で転送するようにしたときの確認項目をまとめたものです。DevPickはバックエンドがFastAPI、フロントエンドがNext.jsのApp Routerですが、確認項目の多くは構成に依存しません。

確認1:統合した時点で「旧ID→統合先ID」を残しているか

レコードを物理削除すると、そのIDが何に統合されたかは、後からはもう分かりません。転送するには、統合する処理の中で対応を書き残しておく必要があります。

DevPickでは、旧記事IDを主キー、統合先の記事IDを値に持つ転送用のテーブルを足し、統合処理が旧記事を削除するのと同じトランザクションで1行書き込むようにしました。統合がロールバックされれば、対応の記録も一緒に消えます。

この仕組みを入れる前に統合したレコードは、対応がどこにも残っていません。ログや調査メモなどから旧IDと統合先が分かるものは、テーブルを作るマイグレーションで初期データとして入れておきます。

確認すること:

  • 対応の記録を、削除と同じトランザクションで書いているか
  • 仕組みを入れる前に統合した分を、調べられる範囲で埋めたか

確認2:統合が重なったとき、転送が何段も連なる形になっていないか

AをBに統合したあと、BがさらにCに統合されることがあります。対応をそのまま足していくと、Aを開くとBへ転送され、Bを開くとさらにCへ転送される、という連鎖になります。検索エンジンが辿るリダイレクトの段数には限りがあるので、段数は少ないほうが安全です。

DevPickでは、書き込むときに連鎖をまとめています。BをCに統合するとき、「転送先がBになっている行」をすべてCへ付け替えてから、B→Cの行を書きます。これで、Aの行もA→Cになります。

読み取る側でも、念のため対応表を辿れる段数に上限を設け、同じIDに戻ってくる循環を見つけたら転送をやめるようにしています。

確認すること:

  • 統合先がさらに統合されたとき、既存の対応を新しい統合先へ付け替えているか
  • 読み取り側で、段数の上限と循環の検出を入れているか

確認3:転送先が消えたとき、404に戻っているか

統合先も、保持期間切れなどでいずれ削除されます。DevPickでは、記事を90日の保持期間で消しています。

そのときに対応表だけを見て転送すると、転送した先が404になります。DevPickでは、対応表を辿り終えたあとに統合先の記事が実在するかを確かめ、無ければ転送せず404を返すようにしました。

確認すること:

  • 転送する前に、最終的な転送先が存在することを確かめているか

確認4:「存在しない」と「見せられない」を分けて扱っているか

詳細APIが404を返す理由は、1つとは限りません。DevPickでは、記事が存在しない場合のほかに、他の利用者が登録した非公開の記事を開いた場合も404を返します。

対応表を引くのは、記事が存在しない場合だけにしました。記事が存在していて権限が無い場合は、これまでどおり404です。「404なら対応表を引く」という1つの分岐にすると、権限が無くて404になった記事についても対応表を引くことになり、どちらの理由の404かで振る舞いを分けられなくなります。

確認すること:

  • 対応表を引く条件が「レコードが存在しない」だけになっているか

確認5:SSRで内部APIを呼ぶとき、APIの転送を自動で追っていないか

DevPickの記事詳細ページは、Next.jsのサーバー側でバックエンドの詳細APIを呼び、その結果でページを描画します。バックエンドのAPIも、旧IDに対しては308を返すようにしました。

ここで、サーバー側のfetchが既定のまま転送を追うと、APIの308に従って統合先の記事を取得し、旧URLのまま統合先の内容を200で描画します。読者には同じ内容が見えるので一見問題ありませんが、検索エンジンには、旧URLと統合先のURLに同じ内容のページが2つあるように見えます。

DevPickでは、ページから呼ぶfetchにredirect: "manual"を付けて、APIの308をそのまま受け取るようにしました。APIのLocationヘッダーはAPIのパス(/api/news/articles/{id})を指しているので、ページの転送先には使えません。統合先のIDを独自のレスポンスヘッダーでも返しておき、ページ側はそのIDから、日本語版なら/articles/{id}、英語版なら/en/articles/{id}を組み立てて、permanentRedirectで転送します。

確認すること:

  • サーバー側のfetchが、内部APIの転送を自動で追っていないか
  • ページの転送先を、APIのパスではなくページのURL(言語ごとのURLを含む)から組み立てているか

確認6:本番で、実際に308のステータスが返っているか

Next.jsのpermanentRedirectは、通常は308のレスポンスを返します。ただし公式ドキュメントには、ストリーミング中に呼んだ場合はmetaタグを挿入して、クライアント側で転送すると書かれています(permanentRedirect)。この場合、HTTPのステータスは308になりません。

ブラウザで開くとどちらも統合先へ移るので、見た目では区別できません。DevPickでは、本番で旧URLのステータスと転送先をcurlで確かめました。

$ curl -s -o /dev/null -w '%{http_code} %{redirect_url}\n' https://devpick.it-araiguma.com/articles/60068
308 https://devpick.it-araiguma.com/articles/60083
$ curl -s -o /dev/null -w '%{http_code} %{redirect_url}\n' https://devpick.it-araiguma.com/en/articles/60068
308 https://devpick.it-araiguma.com/en/articles/60083

統合先の/articles/60083は200、対応表に無い存在しないIDは404のままであることも合わせて確認しています。

なお、恒久的な転送には301もあります。Googleは301と308をどちらも恒久的なリダイレクトとして扱います(リダイレクトとGoogle検索)。DevPickはpermanentRedirectが返す308に揃えました。

確認すること:

  • 旧URLをcurlで開き、ステータスが308(または301)になっているか。200でmetaタグが返っていないか
  • 言語ごとのURLそれぞれが、同じ言語の統合先へ転送されているか

まとめ

  • 統合で消したURLは、統合先へ308で転送する。対応は、削除と同じトランザクションで書き残す。仕組みを入れる前の統合分は、分かる範囲で初期データにする
  • 統合先がさらに統合されたら、既存の対応を新しい統合先へ付け替えて、転送を1段に保つ
  • 転送先が消えたら、転送せずに404に戻す
  • 対応表を引くのは「レコードが存在しない」場合だけにし、権限による404とは分ける
  • SSRで内部APIを呼ぶなら、APIの転送を自動で追わず、ページのURLへの転送に組み立て直す
  • 本番でcurlを使い、308のステータスが返っていることを確かめる

統合した記事の旧URLを、最初は「削除」として扱っていた経緯は、Zenn版に書いています。

0
0
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?