While rereading the conversion script for my articles, I noticed a spot that fixes broken links without telling anyone.
When it finds a link to an article that doesn't exist, it removes the link and keeps the text. That's a sensible way to avoid a 404, and the behavior itself is correct. What I had never done was count how many links it removes, and from which articles.
Once I counted, 160 of the 913 Japanese articles held 267 broken links in total. This post records the audit script I wrote, and why I decided not to repair them by machine afterwards.
What the build actually strips
The step uses a regular expression to find Markdown links. If the target MDX file exists in neither language, it deletes the link syntax and leaves the visible text.
const pattern = /\[([^\]]*)\]\(\/(?:(?:ja|en)\/)?articles\/([^)#\s]+)\)/g;Rereading it, I found three things.
- It only matches Markdown links whose parenthesized target starts with
/articles/. HTMLhref="…"is not touched (in what I counted, brokenhreflinks were zero) - The check passes if the target exists in either language, so a link to a page that exists in only one language goes through
- It makes no exception for code blocks
I checked the third point with a small Node script.
// strip_probe.mjs — apply the build's regex to a document containing a code fence
const pattern = /\[([^\]]*)\]\(\/(?:(?:ja|en)\/)?articles\/([^)#\s]+)\)/g;
const link = "[the settings guide](" + "/articles/claude-code/no-such-article)"; // written inline, this very post would be stripped
const fence = "`".repeat(3);
const doc = `See ${link}.\n\n${fence}md\nRelated: ${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);Running it, both the body link and the sample inside the fence became plain text. If you write a post that explains link syntax, your example may reach readers as a bare string.
My count agreed: four such snippets in the Japanese articles and five in the English ones were being stripped inside code blocks.
Listing what gets stripped
Before fixing anything, I wanted to see it. The script below is read-only and uses exactly the same regular expression as the build. If you alter the pattern even slightly, your numbers drift from what the build removes, so copy it as is.
#!/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)Run python3 -I link_audit_mdx.py ja from the repository root for Japanese, or en for English. (The report text is in Japanese; the numbers are what matter.)