20 broken links: three upstream repos restructured at once
Site-wide link health check on the wiki after the latest deploy: 12,753 <a href> occurrences across 311 built HTML pages. Internals resolved clean against the static filesystem; 20 of 463 unique external URLs returned 4xx, plus 1 DNS failure and 3 redirects. Root cause wasn’t content drift — three upstream skill repos restructured in the same week, plus a fourth (Pocock) had restructured a few days earlier. Vercel Labs moved every skill from tree/main/<slug> to tree/main/skills/<slug>; Alireza Rezvani added a skills/ subdir inside the existing engineering/; Corey Haines moved AND renamed 19 of 40 wiki slugs. After fix-pass and re-run of scripts/link-health-check.mjs: 1 broken (intentional, auth-walled), 1 DNS-dead (callouted), 0 unresolved. The script is ~90 seconds, the failure modes are predictable, and preserving wiki URL slugs across upstream renames is the inbound-link-stability move. (Same pattern as the meta-description-sprawl finding earlier in the corpus: a single layout-level cause behind a defect that looked page-shaped.)
What I ran
A re-runnable Node script over the dist/ build output. Reads every *.html file, regex-extracts <a href> values, classifies each link, then verifies:
- Internal (
/-prefixed): filesystem-checksdist/<path>/index.htmlanddist/<path>.html. Strips query and hash before checking. - External (
http(s)://): HEAD request with 12s timeout, follows redirects, falls back to GET on 4xx (some servers reject HEAD). Concurrency 8. Identifying user-agentagentcookbooks-linkcheck/1.0. - Anchor (
#), mailto, tel, javascript: classified but not resolved.
Output: a JSON report at receipts-drafts/link-health-<date>.json plus a console summary. Run: node scripts/link-health-check.mjs. Roughly 90 seconds end-to-end on the 311-page build.
What happened
Top-line:
| Surface | Count |
|---|---|
| HTML files audited | 311 |
Total <a href> | 12,753 |
| Internal | 10,827 (84.9%) |
| External | 1,303 (10.2%) |
Anchor (#...) | 311 (2.4%) |
mailto: | 312 (2.4%) |
Internal: 312 unique paths, 0 broken. The wiki + blog + topic-page graph resolves clean against the build output.
External pre-fix: 463 unique URLs → 20 broken (4xx/5xx) + 1 DNS error + 3 redirects. External post-fix: 462 unique URLs → 1 broken + 1 errored + 3 redirects.
Three upstream repos drove the breakage:
Vercel Labs: 5 → 0
Every skill moved from tree/main/<slug> to tree/main/skills/<slug>. Plus one rename: vercel-deploy-claimable → deploy-to-vercel (v3.0.0, scope broadened from “claimable-ownership deploys” to “deploy to Vercel” with state-aware routing across git push / CLI / link-first).
Wiki adjustment for the renamed one: URL slug stayed at /skills/vercel-deploy-claimable/ for inbound-link stability; body was rewritten to match the new upstream scope; an updatedDate and a “Renamed upstream” callout flag the drift to readers. URL stable, content tracks upstream.
Alireza Rezvani: 3 → 0
Added a skills/ subdir inside the existing engineering/ group: engineering/<slug> → engineering/skills/<slug>. Three skills affected.
Corey Haines: 19 → 0, 57 replacements across 19 files
Largest batch. Upstream not only moved skills under skills/ but also renamed many to shorter forms. 19 of the 40 Haines slugs on the wiki were affected; the other 21 happened to keep their slug unchanged.
A sample of the renames:
| Wiki slug | Upstream rename |
|---|---|
ab-test-setup | ab-testing |
page-cro | cro |
form-cro | cro (consolidated upstream) |
paid-ads | ads |
schema-markup | schema |
popup-cro | popups |
referral-program | referrals |
signup-flow-cro | signup |
The form-cro/page-cro consolidation is content drift, not a link issue. Upstream collapsed two folders into one; the wiki keeps them as two distinct pages because form-level vs page-level CRO are framed as separate concerns on this site. Both Source and attribution links now resolve to the same upstream folder; the wiki simply has two pages where upstream now has one. Worth a content review at some point.
Pocock (fixed in a separate commit earlier in the session)
Same pattern as Vercel: tree/main/<slug> → tree/main/skills/<category>/<slug>. 3 broken → 0.
Total: 27 skill files modified, 1 new audit script committed.
Where it drifted
Two-pass discipline is load-bearing. First sweep flagged 9 broken externals. Applied the fixes. Second sweep — same script, same dist/, no upstream change in between — flagged 11 additional 404s the first run missed. All on Haines paths. Most likely cause: GitHub HEAD responses on a recently-moved repo come back as a mix of 200/302/304 on the first hit (rate-limit and CDN cache coherence), then resolve to clean 404 on the second pass once the edge cache reconciles.
The script was right both times; the upstream signal was noisy. Don’t trust a single HEAD pass against GitHub for link-health audits on a recently-restructured repo. Run twice. The cost is ~3 minutes total; the catch rate goes from ~45% to effectively 100% on the same dist/.
Pattern recognition: aggregator skill repos reorg at ~10 entries. Four of the wiki’s attribution sources moved to a skills/<slug> (or skills/<category>/<slug>) layout in roughly the same window once their flat layout outgrew browsability. Rough cadence looks annual. Plan for it on any future import: any attribution source past ~10 entries is likely to re-org within 12 months. The cheapest mitigation is to bake the link-health re-run into the post-import checklist and the quarterly review.
One DNS failure intentionally left as a callout, not a fix. https://bgpt.pro/mcp — the BGPT MCP service that the bgpt-paper-search skill depends on — returns ENOTFOUND as of the audit. The domain may have been retired or migrated. The skill page was preserved with a near-top availability callout: “as of 2026-05-13, bgpt.pro DNS fails; service may be down — verify upstream before relying on it.” The K-Dense AI upstream skill folder link is unaffected. Keeping the skill page with a warning beats either deleting it (and breaking inbound links) or scrubbing the warning (and shipping a broken recipe).
One 4xx intentionally left as-is. https://www.zotero.org/settings/keys returns 403 from any anonymous crawler — the page requires login. From a logged-in browser it serves the keys-management UI. The link is intentional and points at the right destination; a crawler hit isn’t representative of the user experience.
What I’d change
Two things to add to the link-health workflow:
- Run the script twice per audit, not once. Detailed above. Three minutes, ~2× the catch rate against recently-moved upstream repos.
- Make
link-health-check.mjspart of the post-attribution-import checklist. Every batch of new skills should run it once at import (catch pre-existing rot) and once at quarterly review (catch fresh rot from upstream changes). 90 seconds, no cost.
What the script doesn’t catch (worth knowing the scope before relying on it as a complete check):
- In-page anchor links (
#section-name) are extracted and classified but never resolved against the rendered headings. A broken#some-headingin the source markdown wouldn’t be flagged. - Images (
<img src>) are out of scope —<a href>only. A broken hero image wouldn’t surface. - JS-injected links (e.g., Pagefind search results) don’t appear in the static HTML at extraction time.
mailto:validity isn’t checked — 312 occurrences passed through unverified.- SSL/cert health for external destinations isn’t checked beyond what HEAD’s underlying TLS handshake reports.
External link health degrades silently when upstream repos restructure. After this audit: 1 broken (intentional), 1 DNS-dead (callouted), 0 unresolved. Re-run scripts/link-health-check.mjs after any new attribution batch and quarterly as cheap maintenance.