◉CLAUDE LABEN
●2.1.302 — fork サブエージェントが claude -p・Agent SDK・デスクトップで既定オンになりました。無効化は CLAUDE_CODE_FORK_SUBAGENT=0(10/16)●2.1.301 — 使用量上限に達した無人実行の再試行を、待ち続けず自動で止めるようになりました(10/15)。最新は 2.1.303(10/18)●SONNET 4.5 — Claude API での提供終了は 11/30、残り41日。移行先は Sonnet 5.5●WINDOWS — 日本語の環境でだけフックが黙って壊れる報告があり、文字コードの確認が呼びかけられています●FAST MODE — Opus 5.5 の Fast mode がリサーチプレビューを終え、ベータヘッダーが不要になりました(10/15)●NEW — Cowork に渡したフォルダの先頭に、十行の約束ファイルを置いてみました●2.1.302 — fork サブエージェントが claude -p・Agent SDK・デスクトップで既定オンになりました。無効化は CLAUDE_CODE_FORK_SUBAGENT=0(10/16)●2.1.301 — 使用量上限に達した無人実行の再試行を、待ち続けず自動で止めるようになりました(10/15)。最新は 2.1.303(10/18)●SONNET 4.5 — Claude API での提供終了は 11/30、残り41日。移行先は Sonnet 5.5●WINDOWS — 日本語の環境でだけフックが黙って壊れる報告があり、文字コードの確認が呼びかけられています●FAST MODE — Opus 5.5 の Fast mode がリサーチプレビューを終え、ベータヘッダーが不要になりました(10/15)●NEW — Cowork に渡したフォルダの先頭に、十行の約束ファイルを置いてみました
記事一覧/Claude Code
⟐ Claude Code/2026-10-20上級

ビルドが黙って剥がしていたリンク切れ267本を数え、類似スラッグでの自動修復を見送りました

MDX の記事ビルドが切れた内部リンクを黙って除去していました。除去の一覧を出す監査スクリプトと、類似スラッグで直すのを見送った判断、件数を増やさない基準線の置き方をまとめました。

Claude Code270MDX内部リンクNext.js9品質管理3

✦ プレミアム記事

記事の変換スクリプトを読み直していて、リンク切れを黙って直している箇所に気づきました。

存在しない記事へのリンクを見つけると、リンクだけを外して文字は残す処理です。404 を出さないための配慮で、挙動としては正しいものでした。ただ、その処理が 何本のリンクを、どの記事から外しているのか を、私は一度も数えていませんでした。

数えてみると、日本語版 913 本のうち 160 本の記事に、合計 267 本の切れたリンクがありました。そのときに書いた監査スクリプトと、数えたあとに「機械で直さない」と決めた理由を、書き残します。個人開発のサイトで、記事が増えるほど静かに溜まっていく種類の問題だと感じています。

変換スクリプトは何を剥がしているのか

該当の処理は、正規表現でマークダウンのリンクを拾い、リンク先の MDX が日英どちらにも存在しなければ、リンク記法だけを本文から消します。

const pattern = /\[([^\]]*)\]\(\/(?:(?:ja|en)\/)?articles\/([^)#\s]+)\)/g;

読み返して分かったことが三つあります。

  • 対象は、角括弧のリンクテキストに丸括弧の /articles/… が続く、マークダウンのリンクだけです。HTML の href="…" は拾いません(ただし、今回数えた範囲では href 形式の切れは 0 件でした)
  • 判定は「日英どちらかに存在すれば OK」です。片方の言語にしかない記事へのリンクは通ります
  • コードブロックの中も区別しません

三つ目は、手元で確かめました。次のような小さな Node スクリプトです。

// strip_probe.mjs — ビルドと同じ正規表現を、コードブロック入りの文書に当てる
const pattern = /\[([^\]]*)\]\(\/(?:(?:ja|en)\/)?articles\/([^)#\s]+)\)/g;
const link = "[設定ガイド](" + "/articles/claude-code/no-such-article)"; // 記事内で直接書くと、この記事自体が剥がされる
const fence = "`".repeat(3);
const doc = `本文の ${link} です。\n\n${fence}md\n関連: ${link}\n${fence}\n`;
let out = doc;
for (const m of doc.matchAll(pattern)) out = out.split(m[0]).join(m[1]);
console.log(out);

実行すると、本文のリンクも、コードブロックの中の見本も、同じように「設定ガイド」という文字だけになりました。リンク記法の書き方を解説する記事を書いたとき、サンプルが読者の画面でリンクのない文字列に変わっている可能性があります。

実際、今回の集計でも、コードブロックの中で剥がされている記法が日本語版に 4 本、英語版に 5 本ありました。

剥がされているリンクを一覧にする

直す前に、まず見えるようにしたいと考えました。ビルドと まったく同じ正規表現 で数える、読み取り専用のスクリプトです。正規表現を少しでも変えると、ビルドが剥がす数と食い違うので、コピーして使います。

#!/usr/bin/env python3
"""内部リンク監査: ビルドが黙って剥がしているリンクを一覧にする(読み取り専用)。"""
import difflib, glob, re, sys
from collections import Counter, defaultdict
 
LANG = sys.argv[1] if len(sys.argv) > 1 else "ja"
MAX = int(sys.argv[sys.argv.index("--max") + 1]) if "--max" in sys.argv else None
ROOT = f"content/articles/{LANG}"
LINK = re.compile(r"\[([^\]]*)\]\(/(?:(?:ja|en)/)?articles/([^)#\s]+)\)")  # ビルド側の正規表現と同一
FENCE = re.compile(r"```.*?```", re.S)
 
exists, by_cat = set(), defaultdict(list)
for p in glob.glob(f"{ROOT}/*/*.mdx"):
    cat, slug = p.split("/")[-2], p.split("/")[-1][:-4]
    exists.add(f"{cat}/{slug}"); by_cat[cat].append(slug)
all_slugs = [s for v in by_cat.values() for s in v]
 
cited, citing_files, in_fence = Counter(), defaultdict(set), []
for p in sorted(glob.glob(f"{ROOT}/*/*.mdx")):
    text = open(p, encoding="utf-8").read()
    fenced = [(m.start(), m.end()) for m in FENCE.finditer(text)]
    for m in LINK.finditer(text):
        if m.group(2) in exists:
            continue
        cited[m.group(2)] += 1
        citing_files[m.group(2)].add(p)
        if any(a <= m.start() < b for a, b in fenced):
            in_fence.append((p, m.group(0)))
 
files = set().union(*citing_files.values()) if citing_files else set()
print(f"[{LANG}] 記事 {len(exists)} 本 / 切れ先 {len(cited)} 種 / 切れリンク {sum(cited.values())} 本 / 該当記事 {len(files)} 本")
print(f"コードブロック内の切れリンク記法: {len(in_fence)} 本")
 
rows = []
for target, n in cited.items():
    slug = target.split("/")[-1]
    near = difflib.get_close_matches(slug, all_slugs, n=1, cutoff=0.0)[0]
    rows.append((difflib.SequenceMatcher(None, slug, near).ratio(), target, n, near))
rows.sort(reverse=True)
for th in (0.9, 0.8, 0.7):
    print(f"類似度 {th} 以上の候補: {sum(1 for r in rows if r[0] >= th)} 件")
print("--- 類似度の高い順(機械では直さない。目視用)")
for r, t, n, near in rows[:5]:
    print(f"{r:.2f}  {t}  ->  {near}")
print("--- 引用が多い切れ先(書く価値の目安)")
for t, n in cited.most_common(3):
    print(f"{n:>2} 本から  {t}  ({len(citing_files[t])} 記事)")
 
if MAX is not None and sum(cited.values()) > MAX:
    print(f"NG: 切れリンクが基準 {MAX} 本を超えました")
    sys.exit(1)

python3 -I link_audit_mdx.py ja で日本語版、en で英語版を数えます。リポジトリの直下で実行します。

✦

ここまでお読みいただきありがとうございます。

この記事の続きを読む

この先には、実装コードやベンチマーク結果など、実務でお役に立てる内容をご用意しています。このサイトは広告を掲載しておらず、サーバーや開発にかかる費用はメンバーの皆様のご支援で成り立っています。もしお役に立てていましたら、ご支援いただけますと大変ありがたいです。

この記事で得られること
✦自分のサイトのビルドが黙って剥がしているリンクを、記事を書き換えずに一覧にして、どの記事に何本あるかを数えられるようになる
✦切れ先に「近い名前」の記事があるとき、機械で置換してよいかを類似度ではなく目視の基準で判断できるようになる
✦切れリンクが静かに増え続ける前に基準本数を決めて止める仕組みを置き、後から数百本を掘り返す時間を省けるようになる
Stripe による安全な決済 · いつでもキャンセル可能
✦

この記事を購入する

この先の内容をすべてお読みいただけます。一度のご購入で、いつでも何度でもアクセスできます。このサイトは広告を掲載しておらず、皆さまのご支援がサーバー費用などの運営を支えています。

または
メンバーシップなら全記事が読み放題 →
シェア

お読みいただきありがとうございます

Claude Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • ✦コピー&ペーストで使える実装コード付き
  • ✦毎日新しい上級ガイドを追加
  • ✦¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

関連記事

⟐ Claude Code2026-08-19
一度キャッシュに載った500ページは、直しても消えません
修正をデプロイしたのに壊れたページが出続けたことがあります。原因はコードではなくエッジキャッシュでした。ステータスコードだけを見る保存判定が、200で返る壊れたHTMLを通していたのです。本文の完全性で保存可否を決めるガードの実装と、運用で調整した誤検知の線引きを、実際に踏んだ経緯とともに書きました。
⟐ Claude Code2026-10-20
席を離れて戻ると会話に要約が差し込まれていた — アイドル圧縮の見分け方と、消したくない細部の残し方
v2.1.299 で入ったアイドル圧縮により、約54分放置した長い会話は自動で要約されます。何が起きたかの見分け方、止め方、そして要約に消されたくない前提を CLAUDE.md へ退避させる手順をまとめます。
⟐ Claude Code2026-10-19
375px で横に 90px はみ出した朝、犯人は querySelectorAll に映らない疑似要素でした
スマホ幅でページが横に広がる原因が ::after だったとき、要素の走査では見つかりません。疑似要素まで差分で確定する観測スクリプトと、sticky を壊さない直し方を実測つきでまとめました。