Two commits, each reviewable on its own: the first makes a refused link say where it was written, the second reports all of them instead of the first.
1. Name the document a refused link was authored in
Both link failures in resolveLink named the destination and nothing else, which is the
half the reader already knows: the destination is what is missing, and the file holding
the link is what has to be edited. On a docs set of any size the only way to act on the
message was to grep for the string.
The throw site already had the answer. resolveLink reads page.sourceFile on the line
above, to resolve the destination it then rejects:
error: guides/start.md: Markdown link has no documentation source: ../adr/absent.mdRelative to the documentation root, because an absolute temporary path is noise in a
build log. Under an include it names the included file rather than its host, because
rewriteMarkdownSourceLinks hands resolveLink the innermost origin on its stack. That
is the file somebody has to open, and the two differ exactly when the message matters
most, so there is a test for it.
2. Report every refused link, not the first
A documentation set with N dead links took N builds to clean up: fix one, rebuild, learn the next. Each run is a full build and the count is unknowable until the last one passes. That is felt hardest on the first build after adopting the check, when a set that has never been link-checked is checked all at once.
resolveLink now takes an optional collector. With one it records the failure and
returns the destination untouched; without one it throws exactly as before, which keeps
the dev server failing fast: it renders a single page on request and has no later point
to report at. The build passes a collector, renders every page, and throws once:
4 Markdown links could not be resolved:
guide.md ./missing-one.md (no documentation source) (3 times)
other.md ./missing-two.md (no documentation source)Repeats are grouped with a count rather than printed as identical rows. There is no line number: by the time links are resolved the markdown has already become HTML, so the authored position is gone, and a fabricated one would be worse than none. The case this came from was three links to one missing file in a single document.
Nothing changes about what is published
The throw is still inside withBuildOutput, before anything is moved into place, so a
build with a dead link still refuses and still leaves the previous complete output
untouched. The existing tests covering that behaviour pass unchanged. Returning the
destination untouched during a collecting render is safe for the same reason: that HTML
is never published, and leaving it alone keeps the failing page readable while it is
being looked at.
Tests
Five, in test/markdown-source-links.test.ts beside the existing coverage for this area.
Each behavioural one was run against the unpatched source first and fails there:
Expected substring: "guides/start.md: Markdown link has no documentation source: ../adr/absent.md"
Received message: "Markdown link has no documentation source: ../adr/absent.md"
Expected to contain: "3 Markdown links could not be resolved"
Received: "Error during build: Error: guides/start.md: Markdown link has no documentation source: ./absent-one.md"Full suite: 615 pass, 0 fail. Lint unchanged at the 3 pre-existing warnings in README.md
and serve.ts, verified identical with and without these changes. tsc --noEmit clean.
🤖 Generated with Claude Code