連載の記事に「次回:〇〇」というリンクを書いています。そこを押すと404でした。
リンク切れは事前に確認していました。結果は0件でした。確認の仕方が間違っていたのです。
存在するのに辿り着けないリンクの話です。確認が通ってしまった理由、直し方を3つ比べて1つを選んだ過程、そして表示するときに判定する実装を書きます。
何を数えていたか
確認の中身はこうでした。全記事の本文から /blog/ で始まるリンクを抜き出し、その行き先のスラッグがCMSに存在するかを見る。存在しないものがあれば、それがリンク切れです。
この方法で、112本ぶんを突き合わせて0件でした。
CMSの中には、公開済みの記事と、予約投稿の記事が両方入っています。
連載の第1回を公開した時点で、第2回はまだ予約投稿の状態です。CMSには在るので、確認は通ります。しかしサイトに出ているのは公開済みのものだけなので、押すと404に着きます。
2つの集合がずれている
整理すると、こうなっていました。
| 含まれるもの | |
|---|---|
| CMSにある記事 | 公開済み + 予約投稿 + 下書き |
| サイトに出ている記事 | 公開済みだけ |
確認していたのは上の集合で、読む人が見ているのは下の集合でした。差の部分が、そのまま404になります。
書いている側からは見えません。管理画面では第2回が普通に並んでいますし、編集もできます。書き手の視界には、公開前の記事も入っているからです。
直し方を3つ比べた
| 案 | やめた理由 / 選んだ理由 |
|---|---|
| 公開のたびに本文からリンクを外す | 連載は5本あり、回が進むたびに手を入れる。忘れた回だけ404が残る |
| 「次回」を書かない | 404は消えるが、次があることも伝わらなくなる。連載として損 |
| 表示するときに判定する | 採用。書く側は常に「次回」を書ける。公開日が来れば自動でリンクに戻る |
1つめを外したのは、人が覚えていることを前提にした手順だからです。今回そもそも見落としたのは、覚えていることを前提にした確認をしていたからでした。同じ形の対策を足すと、同じ理由で破れます。
本文を通す場所が、もう1段増えた
このサイトの本文は、表示する前にいくつかの処理を順番に通ります。商品カードを差し込み、リンクの末尾の形を揃え、見出しに目印を付ける。そこに1段足しました。
const posts = await getPosts();
// getPosts() は公開済みのものだけを返す。
// ここに無いスラッグは「CMSには在るが、まだ出ていない」記事になる。
const publishedSlugs = new Set(
posts.map((p) => p?.slug).filter((s): s is string => Boolean(s)),
);
const rawContent = softenPendingLinks(
normalizeInternalLinks(injectProducts(post.content)),
publishedSlugs,
);
判定の材料はもともと手元にありました。前後の記事と関連記事を出すために、記事一覧をすでに取っています。取得の順番を入れ替えただけで、通信は1回も増えていません。
リンクを消さずに、文字にした
return html.replace(POST_LINK, (whole, _before, slug, _after, text) => {
if (publishedSlugs.has(slug)) return whole;
return `<span class="pending-link">${text}<span class="pending-link-note">(近日公開)</span></span>`;
});
行き先が無いなら丸ごと消す、という手もありました。選ばなかったのは、予告そのものには意味があるからです。
「次回:打ち方が違っても、同じ言葉として扱う」という一文は、続きがあることと、その内容を伝えています。押せるかどうかとは別の仕事です。
消すと、その情報まで一緒に消えます。薄い文字にして「(近日公開)」を添えれば、押せないことは分かり、何が来るかは残ります。
「(近日公開)」をCSSの装飾ではなく本文の一部として出しているのは、読み上げや、文字を選んでコピーしたときに落ちないようにするためです。押せない理由は、飾りではなく中身として持たせることにしました。
置き換えで壊しそうな場所
本文のHTMLを書き換える処理は、意図しないものまで巻き込みます。6つ試しました。
| 試したこと | 期待 |
|---|---|
| 公開済みへのリンク | そのまま残る |
| 1つの行に「前回」と「次回」が並ぶ | 前回は残り、次回だけ文字になる |
class や title が前後に付く |
拾える |
連載の目次(/series/)へのリンク |
触らない |
| 外部サイトのURL | 触らない |
リンクの文字に <strong> が入る |
中のタグを保つ |
5つめは見落としやすい形をしています。外部のURLにも /blog/ という並びが現れることがあるので、行き先がサイト内かどうかを先に判定する必要がありました。
確認そのものを直す
今回いちばん効いたのは、コードの修正ではありません。確認の条件を書き直したことです。
| 見ていたもの | |
|---|---|
| 前 | リンク先のスラッグが、CMSに存在するか |
| 後 | リンク先のスラッグが、公開済みの一覧に在るか |
条件を書き直して数え直したら、公開中の57本のうち、公開前を指しているリンクは1件でした。指摘を受けたその1件です。数が合ったことで、見落としの範囲が確定しました。
公開日が来たら、本当に戻るのか
「公開日が来れば自動でリンクに戻る」と書きましたが、これは作った側の見込みでしかありません。確かめる機会は、2日後に来ました。
第2回の公開日に、第1回と第2回を両方開いて、連載の案内を見比べました。
| 記事 | 「次回」の行き先 | 表示 |
|---|---|---|
| 第1回 | 第2回(その日に公開) | リンクに戻っていた。押すと開く |
| 第2回 | 第3回(まだ予約) | 文字のまま、「(近日公開)」付き |
第1回の「次回」は、2日前まで押せない文字でした。その日の朝、誰も何も触らないまま押せるリンクになっていて、代わりに第2回の「次回」が押せない文字になっていました。1本ずつ、順送りに開いていく形です。
押せなくする側だけを見て終わりにしていたら、「公開後もずっと押せないまま」という別の壊れ方に気づけませんでした。
表示を止める仕組みは、止めたものを再開させる側まで確認してようやく半分ではなくなります。
今回の学び
まず、存在することと、辿り着けることは別だということ。手元のデータで「在る」と確認できても、読む人に配っている集合に入っていなければ意味がありません。確認は、読む人が見るほうの集合でやる必要がありました。
次に、書き手の視界は広すぎること。管理画面には公開前のものも並びます。そこを基準に考えると、自分にだけ見えているものを、全員に見えていると錯覚します。
そして、人が覚えていることを前提にした対策を足さないこと。見落としの原因が「覚えていること前提の確認」だったのに、対策も同じ形にしたら、同じ理由で破れます。表示するときに判定する形にしたのは、そこを繰り返さないためでした。









