Windowsで内部リンクを検査する ― ページ・見出し・大文字違いを3例で確認

このブログでは、記事を整理したときに内部リンクの訂正が必要になり、生成済みHTMLを確認する検査を加えました。ところが、その検査自体にも見逃しがありました。Windows上で、実ファイルがsampleなのにリンクだけSampleとしても、「存在する」と判定していたのです。
そこで、ファイルシステムの存在確認だけに頼らず、実際のファイル名を文字列として照合する方式へ変更しました。この記事では、壊した3例を検出するまでの手順と、読者が再実行できる小さな検査ツールを公開します。公開サーバーへ通信する検査ではなく、ビルド済みHTMLの静的検査です。
検証条件と、何を調べるツールか
最初の検証は2026年9月5日、追試は2026年9月11日に、いずれもWindows・Python 3.14.2で実行しました。追加のPythonライブラリは使っていません。コードはPython 3.11以降の標準ライブラリを使う構成ですが、今回実行したのは3.14.2のみです。
再現用ZIPをダウンロードして展開すると、検査コード、12件のテスト、説明書、修正前後のHTML例が入っています。リンク先は人工のページと予約された例示用ドメインです。実際の障害件数やAIの正答率を表すデータではありません。
ツールはa要素のhrefを読み、指定した公開元と同じURLについて、次を確認します。
- 対応するファイルがビルド先に存在するか。大文字・小文字も一致させる。
#確認のような見出し指定があれば、HTMLに対応するIDがあるか。- 対象外のリンクは検査済みにせず、除外した件数を表示する。
/guide/はguide/index.htmlへ対応する前提です。独自の転送設定や動的な画面を持つサイトでは、そのまま適用できるとは限りません。対象は、ドメイン直下に置く静的サイトのビルド結果です。
3つの失敗を含むページで実行する
展開したフォルダをPowerShellで開き、次を実行します。元のHTMLを変更したり、公開サイトへ送信したりする操作はありません。
python link_audit.py broken --origin https://example.com
$LASTEXITCODE
broken/index.htmlには6リンクを用意しました。このうち意図的に壊したのは次の3つです。
<a href="/gone/">存在しないページ</a>
<a href="/guide/#gone">存在しない見出し</a>
<a href="/Guide/">大文字だけ違うパス</a>
実ファイルはguide/index.htmlで、見出しのIDは確認です。実行結果では、同じ公開元の5リンクを調べ、外部の1リンクを対象外にし、3件の問題を検出しました。終了コードは1でした。
| リンク | 検出された理由 |
|---|---|
/gone/ |
missing_target:対応するファイルがない |
/guide/#gone |
missing_fragment:ページはあるが、そのIDがない |
/Guide/ |
missing_target:実ファイルの小文字と一致しない |
残りの内部2リンクは、日本語IDをURLエンコードしたものと、クエリ付きの相対パスです。この固定入力では両方とも問題なしでした。日本語を含むあらゆるURLや特殊な配信設定まで保証する結果ではありません。
同じページを修正して比較する
fixed側では、3か所のリンク先を実在する/guide/と/guide/#確認へ直しました。外部リンクはそのまま残しています。
python link_audit.py fixed --origin https://example.com
$LASTEXITCODE
python -m unittest test_link_audit.py
9月5日の実行結果は次のとおりです。
| 項目 | 修正前 → 修正後 |
|---|---|
| 読み込んだHTML | 2 → 2 |
| 調べた内部リンク | 5 → 5 |
| 対象外の外部リンク | 1 → 1 |
| 検出した問題 | 3 → 0 |
| 終了コード | 1 → 0 |
付属の単体テスト12件も成功しました。日本語ID、相対リンク、大文字違い、ポート違い、非HTTPリンクなどを含みます。ただしテストに成功しても、未収録の入力まで正しいという証明にはなりません。
9月11日の追試でも同じ結果になった
公開原稿へ移す前に、配布するZIPを別の一時ディレクトリへ展開し、上の3コマンドを2026年9月11日に再実行しました。Pythonは3.14.2、入力ファイルは9月5日版と同じです。
broken:HTML 2、内部リンク5、対象外1、問題3、終了コード1fixed:HTML 2、内部リンク5、対象外1、問題0、終了コード0python -m unittest test_link_audit.py:12件成功、終了コード0
9月5日から件数の差はありませんでした。これは同じ固定入力を同じWindows環境で再実行した継続確認であり、別OSや別サイトでの再現性を示す比較ではありません。
さらに、この記事と配布物を含めてAstroをビルドした直後のdistにも同じツールを実行しました。64 HTML・内部リンク1,411件は問題0、公開元が異なる612件は対象外でした。対象外には外部URLが含まれるため、612件を到達確認済みとは数えていません。
上図は9月11日の出力件数を読みやすく配置した生成図で、生ログの画像ではありません。コマンドとJSON出力の項目名を残しているため、ZIPを展開して同じ数値になるか確認できます。
既存の検査が見逃した、大文字違いの原因
旧チェッカーは、リンクから作ったローカルパスに対してis_file()などで存在を調べていました。今回のWindows環境では、/posts/Sample/というリンクが小文字のposts/sample/index.htmlへ一致し、問題0件・PASSと報告されました。
この見逃しを検出するテストを先に追加すると、旧コードでは4テスト中1件が失敗しました。検査ツールはPASSを返しているのに、テストが要求する「大文字違いを検出する」という条件を満たしていなかったためです。
修正版では、ビルド先を列挙して得た相対ファイル名を辞書のキーにし、リンクから求めた文字列と完全一致で比較します。同じ入力を再実行すると、見逃しの回帰テストを含むサイト監査4件が成功しました。単独ツール12件と合わせて16件です。配布ZIPにはサイト固有の監査は含めず、単独で実行できる12件を入れています。
なお、/Guide/がどの公開サーバーでも必ず404になる、と主張する検証ではありません。配信側に転送や大文字を吸収する設定があれば挙動は変わります。今回の検査は、そうした設定に依存せずソースの綴りを実ファイルと一致させるためのものです。実サイトで大文字違いの障害が発生していた、という意味でもありません。
ページの存在だけでは、目次のリンクは確認できない
MDNの説明では、#以降のフラグメントはサーバーへ送られず、取得後にブラウザが処理します。つまり、ページ自体を取得できることと、狙った見出しへ移動できることは別です。
このツールはPythonのHTMLParserで属性を読み、urllib.parseで相対URLの解決と分解を行っています。リンクに含まれる日本語IDはURLデコードして照合します。URLの分解は安全性や実在の検証ではないため、分解できたことだけでPASSにはしません。
9月5日時点のこのブログの生成物にも実行し、61 HTML・内部リンク1,326件について問題0件でした。外部URL395件は対象外です。これは当日の生成物に対する結果であり、記事追加後もずっと同じ件数になるわけではありません。
実務への影響と、自分のサイトで使うときの限界
ビルド出力がdistなら、公開元を自分のドメインに替えて実行できます。
python link_audit.py ./dist --origin https://your-site.example
対象にはビルド出力だけを指定し、ホームフォルダ全体や業務ファイルのフォルダを渡さないでください。出力には相対パスやリンク先が載るため、共有する前に機密情報が含まれていないか確認します。
このツールが調べないものも残ります。
- 外部URLの到達性、ログインが必要なページ、実際のHTTPステータス。
- 画像・CSS・スクリプトのリンク、JavaScriptが後から作るリンク。
- サーバーの転送・書き換え、HTMLの文法、表示崩れ、見出しが固定ヘッダーに隠れる問題。
- テキストフラグメントの検索内容や、PDF内のページ指定。
base[href]など対応していない構成は、無視して合格にせず問題として表示します。PASSはこの限定された検査の結果です。内容の正確さ、読みやすさ、AdSenseの審査適合を示す判定ではありません。
公開前は、機械検査に加えてブラウザで主要な目次・関連記事を実際に開いてください。サイトの作成・公開手順やビルド時間の測定は、別のAstroとCloudflare Pagesの実践記事にまとめています。
2026年9月11日追記:9月5日に作成した配布ZIPを再実行し、壊した3例、修正版、12件の単体テストが同じ結果になることを確認しました。公式仕様の参照先は同日に再確認済みです。