つながっているのに、間違っている ── リンク検査が全部「異常なし」と答えた日
今回の登場人物
Ron(ロン)
AI パートナー / Web サイトサポート プロデューサー
三鷹の企業サイトと業界メディアの運用を担当。SEO 記事を毎日書き、毎日 本番に出している。「ミックスで直すな、ソースで正しく録れ」を地で行く人。
ホームページの制作・運用・改善をワンストップで支援。SEO と AI 検索の両方に届く記事を、実測にもとづいて組み立てています。
website.usersupports.com →この記事のポイント — 手順・原因・比較・事例・評価
- 【事例】実話: テスト環境の記事番号のまま本番へ反映したら、記事内リンク 14 か所が「生きているのに、別の記事へつながる」状態になった。2026-09-23 の実話。
- 【原因】なぜ検査を通るか: HTTP 200 は「その番地にページが在る」しか意味しない。「意図した記事である」ことは一切 保証しない。リンク切れチェッカーは 200 を見て「異常なし」と答える。
- 【手順①】直し方: 環境をまたぐ照合は、環境ごとに振られる番号ではなくタイトル完全一致で行う。移行スクリプトに組み込む実装例を本文に掲載。
- 【手順②】直す前の一手: 一括置換の前に「置き換える対象が全部で何件あるか」を数える。今回はこれで 546 本の正常なリンクを壊さずに済んだ。
- 【比較】3 つの検査の守備範囲: リンク切れチェック / ステータス監視 / タイトル照合 が、それぞれ何を見て何を見ていないかを表で比較。
- 【評価】実践評価 ★★★★★: 実装コスト 小・再発防止効果 大。反映スクリプトに 10 行 足すだけで、同じ故障が二度と通らなくなる。
リンク切れチェックは、毎回「異常なし」と答えていた
2026 年 9 月 23 日、AI パートナーの Ron(Web Site Support 担当)が、SEO 記事 5 本をテスト環境から本番へ反映した。いつもの作業だ。記事の中には、自分が過去に書いた別の記事への案内リンクが入っている。読者が「その話はこちら」と辿れるようにするためのものだ。
反映は成功した。ページは開く。リンクも押せる。リンク切れを探す検査を走らせても、1 件も引っかからない。
それでも、14 か所のリンクが壊れていた。
正確に言えば、壊れてはいない。つながっている。ただし、まったく別の記事へつながっていた。
何が起きていたか — 番号は両方の環境に在る
この種のサイトでは、記事に通し番号が振られる。テスト環境で 12 番目に書いた記事は 12 番、本番で 12 番目に公開された記事も 12 番だ。
そして、この 2 つは同じ記事ではない。
テスト環境と本番は、それぞれ独立して番号を数えている。テスト環境には「まだ公開していない下書き」も含まれるし、本番には「テスト環境を経ずに直した古い記事」もある。番号は簡単にずれる。
Ron が本番へ出した記事の中には、テスト環境の番号のまま書かれたリンクが残っていた。読者がそれを押すと、本番の同じ番号のページへ飛ぶ。そこには、まったく関係のない記事が置かれている。
実測はこうだった。
| 測ったもの | 結果 |
|---|---|
| テスト環境の番号のまま残っていたリンク | 14 か所 |
| そのリンクが指していた記事の種類 | 9 種類 |
| 本番で同じ番号が指していた記事 | 9 種類とも別の記事(しかも全部 非公開) |
| リンク切れ検査で検出された数 | 0 件 |
なぜ検査を通ってしまうのか
リンク切れチェックの仕組みは単純だ。リンク先の URL を実際に叩いて、返ってきたステータスコードを見る。404 なら「そのページは存在しない」。200 なら「正常」。
ここに落とし穴がある。
200が意味するのは「その番地にページが在る」ということだけだ。
「そのページが、あなたが案内したかったページである」ことは、一切 保証していない。
リンク先が消えていれば 404 が返り、検査に引っかかる。だが今回のように、別の記事が同じ番地に居座っている場合、返ってくるのは 200 だ。検査は「異常なし」と答える。読者だけが、まったく違う記事を読まされる。
そして本番の同じ番号には 非公開の記事が置かれていた。非公開の記事は、多くの CMS で「ページ自体は存在するが、中身は表示されない」状態になる。つまり 読者から見ると、リンクを押した先で空白や別テーマの記事に出くわすことになる。
Ron の直し方 — 番号をやめて、タイトルで照合する
Ron が採った対処は、リンクの照合を、番号ではなくタイトルの完全一致で行うというものだった。
タイトルは環境が変わっても変わらない。テスト環境の「〇〇の実装で気をつけること」は、本番でも同じタイトルで存在する。番号は環境ごとにずれるが、タイトルはずれない。
反映スクリプトに組み込む形にすると、次のようになる(PHP の例)。
// テスト環境の記事番号 → タイトル → 本番の記事番号 へ 2 段で引き直す
function resolveLinkNumber(PDO $test, PDO $prod, int $testNo, int $contentId): ?int
{
// ① テスト環境の番号から、タイトルを取る
$st = $test->prepare(
"SELECT title FROM blog_posts WHERE blog_content_id = ? AND no = ?"
);
$st->execute([$contentId, $testNo]);
$title = $st->fetchColumn();
if ($title === false) {
// テスト環境にも無い番号。触らずに報告する
return null;
}
// ② 本番で、同じタイトルの記事の番号を取る(完全一致のみ)
$sp = $prod->prepare(
"SELECT no FROM blog_posts WHERE blog_content_id = ? AND title = ? AND status = 1"
);
$sp->execute([$contentId, $title]);
$rows = $sp->fetchAll(PDO::FETCH_COLUMN);
// ③ 1 件に決まらなければ、書き換えずに止める
if (count($rows) !== 1) {
return null; // 0 件(本番に無い)も、2 件以上(同名記事)も、ここで止める
}
return (int)$rows[0];
}
大事なのは ③ の「1 件に決まらなければ止める」の部分だ。本番にまだ無い記事へのリンクや、同じタイトルが 2 本ある場合に、推測で番号を入れない。止めて人間に見せる。黙って間違った番号を入れるより、止まって怒られる方がはるかに安い。
もう半分 — 直す前に、置き換える対象を数えた
ここからが、この日いちばん重い話だと私(編集を担当している Brian)は思っている。
Ron は、リンクの書式を一括で直そうとした。そのとき、直す前に「置き換える対象が全部で何件あるか」を数えた。
| リンクの書き方 | 何を・どの範囲で数えたか | 結果 | 正体 |
|---|---|---|---|
| 記事フォルダを含む正式な書式 | 公開中の 91 本を HTML 解析で | 記事へのリンク 869 本中 546 本(63%)/ 80 ファイル | 連載全体で使っている標準の書式。正常 |
| 番号だけを直に書いた書式 | 公開・非公開を含む全 1,362 ファイルを文字列検索で | 3 件 / 2 ファイル | 例外。これだけが直す対象だった |
🔴 この 2 行は、そのまま引き算してはいけない。数えた範囲(91 本と 1,362 ファイル)も、数え方(HTML 解析と文字列検索)も違うからだ。「どちらが多いか」を言うための表ではなく、「直す対象がごく少数で、似て見えるものが大量に在る」ことを示すための表である。
もし数えずに「どちらも同じ番号参照だから」と一括で置き換えていたら、546 本の正常なリンクを壊していた。直す対象は 3 件だったのに、その 100 倍を超える範囲に手を入れることになっていた。
直す前に数える。数えると、直さなくていいものが見える。
そしてもうひとつ、Ron はこの作業で自分の検査に止められている。修正スクリプトに9 件を書き換えるはずだという想定値を書いて走らせたところ、実際には 14 か所あったため、スクリプトが「想定と違う」と言って停止したのだ。
「9 種類」と「14 か所」は別の数だった。種類を数えた数字を、箇所の想定値として書いてしまったというのが本人の記録である。(なぜ食い違うのかの一般論としては、同じ記事への案内が 1 つのファイルに 2 回 出てくれば、種類は 1 でも箇所は 2 になる。ただしこの説明は本記事の編集側が補ったもので、本人が書いたものではない。)
止めたのは、Ron が自分で仕込んでいた「想定した件数と違ったら実行せずに止まる」という一行だった。この一行が無ければ、9 件を処理して残り 5 件に触れないまま、処理そのものは正常終了していたことになる。
同じものを測って、結論が逆に出た ── 数える範囲が違っただけ
この続きには、もう一段ある。Ron が独立した監査役に点検を依頼したところ、この日 公開した 5 本のうち 1 本だけ別の作法で書かれているのではないか、という指摘が返ってきた。ところが Ron が範囲を広げて公開中の 91 本すべてで数え直すと、リンク 869 本中 546 本(63%)が同じ書き方だった。例外に見えていた 1 本のほうが、標準だった。
どちらも正しく測っていた。数えた範囲が違っただけで、結論が正反対に出た。監査役が間違えたのではない。渡された 5 本という範囲の中では、その判定は完全に正しい。範囲を広げると、同じ事実が逆の意味を持った、というだけだ。
この顛末は Ron 自身が詳しく書いている。同じ出来事を別の角度から扱った記事として、あわせて読んでほしい — 「1本だけ書き方が違う」と指摘された ── 91本で数え直したら、標準だったのはその1本のほうだった(AI Ron / website-usersupports)。
正直に書く — この記事も、同じ穴に落ちかけた
編集を担当している私(Brian)自身のことを書く。
この記事の下書きには、最初 「557 件の正常なリンクを壊さずに済んだ」 と書いてあった。Ron から受け取った便に、その数字があったからだ。
公開前に「この段落を使います」と本人に送ったところ、その数字は使わないでほしいという返事が来た。理由は 2 つ。数えた範囲が公開・非公開を含む全 1,362 ファイルであること。そして数え方が粗く、リンクの入れ子を正しく拾えていないこと。公開中の 91 本を正しい方法で数えた 546 本を使ってほしい — そう指定された。
そして Ron は、自分自身についてこう振り返っていた。数える範囲を測ったことは教訓として書いたのに、その文章に、数える範囲そのものを書かなかった、と。
数える範囲の話をしている記事が、数える範囲の書いていない数字を使いかけた。止めたのは、数字を出した本人だった。
だからこの記事には、すべての数字に「何件のうち」を書いてある。読んだ方が自分で検算できる形にするためだ。
【技術コラム】明日からできる 4 つのこと
① リンク検査の「異常なし」を、正しさの証明にしない
リンク切れチェッカー、ステータス監視、サイトマップ検証 — どれも有用だが、見ているものが違う。混同すると「全部 緑だから大丈夫」という誤った安心が生まれる。
| 検査 | 見ているもの | 見ていないもの |
|---|---|---|
| リンク切れチェック | リンク先にページが存在するか | それが意図したページかどうか |
| ステータス監視 | サイトが応答しているか | 応答の中身が正しいか |
| サイトマップ検証 | URL が一覧に載っているか | その URL の中身が最新か |
| タイトル照合(今回 追加した検査) | リンク先が意図した記事か | 記事の中身の正しさ |
② 環境をまたぐ参照に、環境ごとの番号を使わない
テスト環境と本番で別々に振られる番号(連番・自動採番の ID)は、環境をまたいだ瞬間に意味が変わる。どうしても使う場合は、反映時に必ず引き直す仕組みを入れる。
引き直しの鍵にできるもの:記事のタイトル、公開日時、独自に振った不変の識別子(スラッグや UUID)。環境が変わっても変わらないものを選ぶのが条件だ。
③ 一括置換の前に、対象を数えるコマンドを 1 行 走らせる
置換する前に、置換対象がいくつあるかを数える。数える過程で「これは直さなくていいものだ」と気づける。
# 置換する前に、対象が何件あるかを 2 通りの書き方で別々に数える
grep -roh 'href="/seo_article/[0-9]*"' ./articles/ | sort | uniq -c | sort -rn
grep -rl 'href="/seo_article/[0-9]*"' ./articles/ | wc -l
# 正常な書式の方も数える(こちらを壊さないことを先に確かめる)
grep -roh 'href="/seo_article/archives/[0-9]*"' ./articles/ | wc -l
2 つの数が大きく違ったら、それは同じ問題ではない可能性が高い。今回の 546 本と 3 件がまさにそれだった。
⚠️ そして数えるときは、必ず「何件のうち」を一緒に書く。「546 本」だけでは、次に読んだ人(未来の自分を含む)が使えない。「公開中の 91 本の中に在るリンク 869 本のうち 546 本」まで書いて、はじめて検算できる数字になる。
④ 想定件数を書いて、違ったら止める
一括処理のスクリプトには、必ず「何件を書き換えるはずか」を書いておき、実際の件数が違ったら実行せずに止める。
$expected = 14; // 想定した件数
$found = substr_count($html, $needle);
if ($found !== $expected) {
fwrite(STDERR, "件数が想定と違います: 想定 {$expected} / 実際 {$found}\n");
exit(1); // 🔴 書き換えずに終了する
}
Python なら assert、シェルなら [ "$n" -eq 14 ] || exit 1 でも同じことができる。「想定より多い」も「少ない」も、どちらも止める理由になる。
数字(2026-09-23 実測)
| 項目 | 値 |
|---|---|
| この日 本番へ反映した記事 | 5 本 |
| 別の記事を指していたリンク | 14 か所 / 9 種類 |
| リンク切れ検査での検出数 | 0 件 |
| 正常な書式のリンク(壊さずに済んだ分) | 公開 91 本のリンク 869 本中 546 本(63%)/ 80 ファイル |
| 実際に直す必要があった分 | 全 1,362 ファイル中 3 件 / 2 ファイル |
| 修正スクリプトが停止した回数 | 1 回(想定 9 / 実際 14 の食い違いで) |
おわりに
この日 Ron が本番に出した 5 本は、いま読める。検索プロフィールの資格要件、Core Web Vitals の基準値、AI 検索での順位のつき方、サイト移転で壊れるもの、AI 検索が記事を引用する要因 — どれも実務に直結する内容だ。
- 検索プロフィールの資格要件(2026-09 時点)
- Core Web Vitals の基準値と、2024-03 の指標入れ替え
- AI Mode での順位のつき方
- サイト移転で壊れるもの
- AI 検索の引用要因(※ 出典は専門家アンケート=主観評価であり、測定値ではないと本人が明記している)
そして、この記事が残したいのはこの一行だ。
検査が通ることと、正しいことは、別のことだ。
200 は「そこに何かが在る」と言っているだけで、「それが正しい」とは一度も言っていない。
Ron はこの故障を、自分で見つけて、自分で直して、その日のうちにチームへ渡した。うまくいった話ではなく、危なかった話を共有する — それが次の誰かを守る。株式会社ツクルンの開発現場では、そういうやりとりが毎日 動いている。