本文へ

開発記録

404 ページはエラーメッセージだけではない:静的サイトで存在しない URL を扱う

記録された HTTP レスポンス、エラーページの HTML、多言語フォールバックから、404.html の生成、404 の返却、閲覧を続けるためのリンクを区別します。ローカルプレビューでは本番ホストを検証できないという限界も整理します。

林予晴
公開
  • Astro
  • HTTP
  • 静的サイト
  • 多言語サイト

404.html を直接開いて「頁面不存在」(ページが存在しません)と表示されても、サイトが存在しない URL を正しく扱えているとは限りません。

原文のノートに記録したローカルの静的プレビューでは、異なる二つのレスポンスを確認しました。存在しない URL へのリクエストは HTTP 404 を返し、404.html への直接のリクエストは HTTP 200 を返しました。同じエラー画面を表示できても、「存在しない URL をどう扱うか」を検証したのは前者だけです。

404 は、二つの責務に分けて確認するようにしています。サーバーは、その URL にコンテンツがないことを正しく伝えます。ページは、何が起きたかを読者に説明し、閲覧を続けられる入口を用意します。エラー画面のイラスト、わかりやすい文言、正しいステータスコードには、それぞれ果たす役割があります。

エラーページのファイルではなく、存在しない URL を試す

Astro では、404.astro または 404.md からカスタムエラーページを生成でき、静的ビルドでは 404.html が出力されます。公式ドキュメントにも、多くのデプロイサービスがこのファイルを見つけて使用するとあります。ただし、HTML を配信するすべてのサーバーが同じルールを採用しているという意味ではありません。[1]

ここには二つの責務があります。ビルドツールは利用できるドキュメントを生成し、サイトを配信するサーバーは、どのリクエストにそのドキュメントを使い、どのステータスコードを返すかを決めます。

そのため、少なくとも次の二つのリクエストは分けて確認する必要があります。

リクエスト 確認したいこと
404.html を直接開く エラーページのファイルが存在し、本文やアセットを読み込めるか
存在しないことが確認できている URL を開く 通常のコンテンツやホームページではなく、エラーレスポンスとしてページを返すか

エラーページのファイルを直接開いたときに 200 が返ったのは、当時のローカルプレビューで実際に観測したファイルアクセスの挙動です。それを根拠に、存在しない URL の処理に失敗しているとは判断できません。判断に使うべきなのは二つ目のリクエストで、ステータスコードとレスポンスの内容を両方読む必要があります。

すべての URL をホームページへ送ると、何が隠れるか

各ページの HTML をすでに生成しているこの静的サイトでは、存在しないパスをすべてホームページへ書き換え、何を表示するかをブラウザーに委ねる必要はありません。その検証で確認したデプロイ設定は、Astro と静的出力ディレクトリを指定しており、そのようなサイト全体の rewrite は追加していませんでした。

たとえば、記事の URL を入力し間違えたときに、サーバーがホームページを HTTP 200 で返したとします。テストには「応答があり、開けた」という記録だけが残るかもしれません。しかし、読者が探していた記事は表示されていません。

別のケースとして、エラーの文言は表示しながら、レスポンスは 200 のままということもあります。Google のドキュメントでは、成功ステータスコードと空のページやエラー内容を組み合わせると、検索システムが soft 404 と判断する可能性があると説明しています。ページ上の「404」という 3 桁の数字は、HTTP ステータスの代わりにはなりません。[2]

ただし、古い URL をすべてエラー扱いすることもできません。このサイトの /work は、互換性のために意図的に残した入口です。記録したローカルプレビューでは 200 を返し、作品の内容を読めて、canonical は /about/work を指していました。これは、適当に入力した存在しないパスとは異なる URL の契約です。

テストの前に、確認する対象が正規ページなのか、互換用の入口なのか、存在しない URL なのかを区別します。そうすれば、意図的に残したコンテンツを 404 処理の不備と誤認せずに済みます。

noindex はインデックスを制御し、ステータスコードは変えない

確認したエラーページでは、title、description、読める本文を残す一方、ホームページの canonical、og:url、代替言語のメタデータ、構造化データは引き継いでいませんでした。エラーページも sitemap に含めていませんでした。ナビゲーションからエラーページを離れることはできますが、ホームページを現在閲覧している通常ページとして示すことはありません。

これはページの位置づけに関する選択です。共通の外枠を使うからといって、存在しない URL をホームページの別バージョンだと示す必要はありません。

当時確認した HTML には、別途 noindex も出力されていました。これはインデックスに関する指示であり、HTTP 200 を自動的に 404 へ変えるものではありません。Google には、実際に 4xx を返す URL に対する別の処理ルールもあるため、両者を一つの判定として扱うべきではありません。[2][3]

Google はまた、noindex を読むにはクローラーがページにアクセスできる必要があり、その前に robots.txt でブロックしてはいけないと説明しています。[3] したがって、head 内に指示があることを確認しても、わかるのはサイトがその指示を出力したことだけです。検索結果がすでに更新されたとはいえません。

正規ページ、互換ページ、公開メタデータ全体の関係については、静的 HTML、canonical、hreflang、sitemap をどう整合させるかも参照してください。このノートでは、存在しない URL が実際にどのようなレスポンスを受け取るかに焦点を絞ります。

共通のエラーページ一つでは、初期表示の言語に制限がある

この実装では、共通のエラーページを一つビルドし、初期 HTML は繁体字中国語でした。その後、ブラウザーのスクリプトが URL の最初のセグメントを読み取り、en なら英語、ja なら日本語、それ以外なら繁体字中国語を選びます。タイトル、説明、lang、ナビゲーションの文言、ホームページへのリンクも更新します。

記録した英語と日本語の存在しない URL へのリクエストでは、HTTP レスポンスはどちらも 404 で、生の HTML もどちらも繁体字中国語のままでした。ブラウザーでスクリプトを実行すると、英語ページの h1 は Page not found になり、ホームページへのリンクは /en を指しました。日本語ページは ページが見つかりません と表示し、リンクは /ja を指しました。URL は変わっていません。

ここでいえるのは、「共通のエラーページにブラウザーのスクリプトで言語別の表示を追加する」ということです。「サーバーが最初から対応言語のエラー HTML を返す」ということではありません。

JavaScript が実行されない場合も、繁体字中国語のエラー説明は残り、ページには 3 言語のホームページへのリンクが別途用意されています。エラーページから離れる選択肢は残せますが、スクリプトなしで利用できる完全な英語・日本語のエラーページではありません。将来、各言語で正しい初期 HTML が必要になれば、ホストがエラードキュメントをどう選ぶかも含めて設計しなければなりません。言語ディレクトリの下にページを追加するだけでは、存在しない URL に対してホストがそれを使うとは証明できません。

読者をエラーページから案内するために、先に URL を書き換える必要はない

ホームページへのリンクを提示することと、読者を自動でホームページへ送ることの違いは、元のリクエストを保つかどうかにあります。

記録した確認では、存在しない URL へのレスポンスは、成功レスポンスを返すエラーページの URL へ先に redirect していませんでした。ブラウザーは元のパスにとどまり、読者は綴りを確認したり、問題のリンクをコピーしたり、自分でホームページへ戻ったりできました。ページ上のホームページへのリンクとナビゲーションはネイティブのリンクであり、別のアクション用スクリプトが行き先を決めるのを待つ必要はありませんでした。

エラーページの言語切り替えも、存在しないコンテンツの論理パスを保持していました。たとえば、英語の存在しない URL から日本語へ切り替えるとき、変えるのは言語プレフィックスであり、存在しないコンテンツを成功ページのように見せるわけではありません。これは当時確認したスクリプトの責務であって、言語を切り替えれば記事が見つかるという意味ではありません。

エラーの原因を説明するよりも、回復のための入口を確実に示すようにしています。読者に URL の確認を促すことも、実在するホームページを案内することもできます。根拠がないのに「記事は削除されました」と断定したり、まだ提供していない検索機能や連絡機能を加えたりはしません。

ローカルプレビューには末尾スラッシュの例外もあった

サイト設定は trailingSlash: 'never' でした。末尾スラッシュのない未知のパスを試すと、ローカル preview はサイトのカスタムエラーページを使いました。同じパスの末尾に / を追加すると、やはり 404 を返しましたが、レスポンスの内容はサイト独自の画面ではなく、Astro の末尾スラッシュに関するエラーページでした。

この違いは、ステータスコードの確認だけでは足りないことを示しています。どちらのリクエストも 404 ですが、タイトル、言語、回復のための入口は異なります。

検証の結論にも限界があります。これは当時のローカル preview におけるパス形式の処理であり、本番ホストでも最初に同じ分岐を通るとは推定できません。プレビューの見た目を揃えるために通常の URL 方針を変えることはせず、未検証のホスト設定を修正済みとして記すこともしませんでした。

当時の確認でわかった範囲

検証では、まず当時のルート、layout、言語スクリプト、デプロイ設定、sitemap を読みました。その後、ビルド成果物を調べ、ローカル preview に実際にリクエストを送り、英語と日本語の存在しない URL を開いて、スクリプト実行後の内容を確認しました。

確認対象 記録された結果
/about/work と互換用の入口 /work どちらも 200 を返し、作品の内容を読めて、canonical は正規 URL を指した
3 言語のプレフィックス配下にある、末尾スラッシュのない未知の URL すべて 404 を返し、redirect はなく、共通の繁体字中国語のエラー HTML を使用した
英語・日本語の未知の URL で JavaScript を実行した後 h1、lang、ホームページへのリンクが対応言語に切り替わり、未知のパスは保持された
同じ未知の URL に末尾スラッシュを付けた場合 すべて 404 を返したが、Astro のパス形式に関するエラーページを使用した
404.html への直接のリクエスト 200 を返した。エラーページのファイル確認だけに使い、未知の URL のテストとは扱わなかった

これらの結果は、本番のデプロイ先ホスト、キャッシュ、すべてのブラウザーや支援技術を対象にしていません。検索エンジンが何らかのインデックス処理を行ったことを証明するものでもありません。

デプロイ後には、実際のドメインで通常の深い階層のルート、互換用の入口、言語プレフィックスや末尾スラッシュの異なる未知の URL に改めてリクエストを送る必要があります。ステータスコード、リダイレクト、本文、回復用のリンクを読み取ります。エラーページのファイルが生成されたことと、ホストがそれを正しく使っていることは、分けて検証すべき二つの結果です。

出典

[1] https://docs.astro.build/en/basics/astro-pages [2] https://developers.google.com/crawling/docs/troubleshooting/http-status-codes [3] https://developers.google.com/search/docs/crawling-indexing/block-indexing