◉CLAUDE LABJP
●2.1.302 — Fork subagents are now on by default in claude -p, the Agent SDK and the desktop app. Turn them off with CLAUDE_CODE_FORK_SUBAGENT=0 (10/16)●2.1.301 — Unattended retries now stop on their own when a usage limit is hit instead of waiting (10/15). Latest is 2.1.303 (10/18)●SONNET 4.5 — Retires on the Claude API on 11/30, 41 days left. Migrate to Sonnet 5.5●WINDOWS — Reports say hooks break silently only on Japanese-locale Windows; check the character encoding first●FAST MODE — Opus 5.5 fast mode is out of research preview and no longer needs the beta header (10/15)●NEW — I Put a Ten-Line Promise File at the To…●2.1.302 — Fork subagents are now on by default in claude -p, the Agent SDK and the desktop app. Turn them off with CLAUDE_CODE_FORK_SUBAGENT=0 (10/16)●2.1.301 — Unattended retries now stop on their own when a usage limit is hit instead of waiting (10/15). Latest is 2.1.303 (10/18)●SONNET 4.5 — Retires on the Claude API on 11/30, 41 days left. Migrate to Sonnet 5.5●WINDOWS — Reports say hooks break silently only on Japanese-locale Windows; check the character encoding first●FAST MODE — Opus 5.5 fast mode is out of research preview and no longer needs the beta header (10/15)●NEW — I Put a Ten-Line Promise File at the To…
Articles/Claude Code
⟐ Claude Code/2026-10-20Advanced

Counting the 267 Broken Links My Build Was Silently Stripping — and Why I Skipped Fuzzy Auto-Repair

An MDX build was quietly removing broken internal links. Here is the audit script that lists them, why I decided against slug-similarity repair, and how to set a baseline so the count never grows.

Claude Code269MDXInternal LinksNext.js9Quality Control

✦ Premium Article

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/. HTML href="…" is not touched (in what I counted, broken href links 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.)

✦

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
✦You can now list the links your own site build silently strips, without rewriting any article, and count how many each page has
✦You will be able to decide whether a similarly named page is a safe replacement target by reading it, instead of trusting a similarity score
✦You can set a baseline count and fail fast when it grows, saving the hours it takes to dig out hundreds of dead links later
Secure payment via Stripe · Cancel anytime
✦

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $10 for lifetime access
View Membership →

Related Articles

⟐ Claude Code2026-08-19
Fixing the Code Doesn't Evict a Broken Page From the Edge Cache
I shipped a fix and the broken page kept serving. The problem was not the code but the edge cache, which had stored a broken HTML response because the store decision looked only at the status code. Here is the integrity guard I now run before every cache write, and the thresholds I had to tune in production.
⟐ Claude Code2026-10-20
You Stepped Away and Came Back to a Summary — Spotting Idle Compaction and Keeping the Details You Can't Lose
Since v2.1.299, Claude Code compacts a long conversation after about 54 idle minutes. Here is how to tell it happened, how to tune or switch it off, and how to move the assumptions you can't afford to lose into CLAUDE.md.
⟐ Claude Code2026-10-19
The Page Was 90px Too Wide at 375px, and the Culprit Never Showed Up in querySelectorAll
When a pseudo-element causes horizontal overflow on phones, scanning elements finds nothing. A probe script that confirms the culprit by diff, and overflow fixes that keep sticky intact, with measurements.