How to validate every link in a markdown document
- Step 1Open the Link Validator — Go to md-link-validator. The tool has no options to configure — it is a single-action validator, so there is nothing to set up before running.
- Step 2Paste your Markdown or drop a .md file — Paste the document into the input box or drop a single
.md,.mdx, or.markdownfile. The validator accepts one file at a time (acceptsMultiple: false); to audit several docs, combine them first with md-merger. - Step 3Run the validation — Trigger the run. The tool scans the text for bare
http(s)URLs and[text](path)relative links, de-duplicates both lists, then begins network-testing external URLs (up to 50) and format-checking every relative path. - Step 4Read the report header — The first line after the title reads
Checked N external URLs and M relative links.followed by the CORS note. Use these counts as a sanity check — ifNis lower than you expect, some links may be written in reference style ([text][ref]), which this regex does not capture. - Step 5Triage the External URLs section — Each probed URL shows
✓(the fetch resolved) or✗ ... (unreachable or CORS-blocked). Treat✗as a candidate, not a verdict — many live sites block cross-origin requests and will show✗. Open suspicious URLs in a browser tab to confirm. - Step 6Fix flagged links and re-run — In the Relative Links section,
✓means the path matches the safe format and?means it does not (spaces, query strings, protocol-relative//, etc.). Correct the source, then re-run to confirm a clean report before publishing.
What the validator extracts and how it tests each kind
The tool runs two regex passes and treats the two link kinds differently. Verified against lib/markdown/markdown-processor.ts validateLinks().
| Link kind | Extraction regex | How it is tested | Report markers |
|---|---|---|---|
| Bare external URL | (?:https?:\/\/)[^\s)>"']+ — any http(s):// run up to whitespace, ), >, quote | Network probe via fetch (browser: mode: no-cors, 5s timeout). First 50 only | ✓ <url> or ✗ <url> (unreachable or CORS-blocked) |
Relative [text](path) link | \[([^\]]+)\]\((?!https?:\/\/)([^)]+)\) — inline link whose target is not http(s) | Format-check only against ^[./\w-]+(\.\w+)?(#[\w-]+)?$. No network, no filesystem | ✓ <path> (matches) or ? <path> (suspicious format) |
| Duplicate of either kind | Both lists wrapped in new Set(...) | De-duplicated before checking — each unique value tested once | Appears once in the report |
Reference-style link [text][ref] | Not matched by either regex | Ignored entirely — never extracted or checked | Absent from the report |
Report structure and download
The output is a single plain-text report, not annotated Markdown. Sections only appear when their list is non-empty.
| Report element | When it appears | Example line |
|---|---|---|
# Link Validation Report title + count line | Always | Checked 12 external URLs and 5 relative links. |
| CORS note | Always (browser path) | > Note: Due to browser CORS restrictions, only DNS/network reachability can be tested, not HTTP status codes. |
## External URLs | Only when ≥1 external URL is found | ✓ https://example.com/docs |
... and N more | Only when more than 50 external URLs exist | ... and 38 more |
## Relative Links (format check only) | Only when ≥1 relative link is found | ? ../my docs/page.md |
| Output file | On download | <name>-link-report.txt (type: report / text/plain) |
Cookbook
Real Markdown inputs and the exact report the validator produces. Reports are plain text — copy them into a PR description or a docs-QA ticket.
A mixed doc with external and relative links
A typical knowledge-base page with a couple of external references and internal links. Both kinds are extracted; the external ones are probed, the relative ones are format-checked.
Input (page.md): See the [API guide](./api/guide.md) and the [changelog](../CHANGELOG.md). Upstream docs live at https://example.com/docs and https://status.example.com. Report (page-link-report.txt): # Link Validation Report Checked 2 external URLs and 2 relative links. > Note: Due to browser CORS restrictions, only DNS/network reachability can be tested, not HTTP status codes. ## External URLs ✓ https://example.com/docs ✓ https://status.example.com ## Relative Links (format check only) ✓ ./api/guide.md ✓ ../CHANGELOG.md
Duplicated URLs collapse to one check
The same external URL appears three times across the doc. The Set de-duplicates it, so the count line says 1 and only one probe runs.
Input: Download it here: https://example.com/dl Mirror: https://example.com/dl Also https://example.com/dl works. Report: # Link Validation Report Checked 1 external URLs and 0 relative links. > Note: Due to browser CORS restrictions, only DNS/network reachability can be tested, not HTTP status codes. ## External URLs ✓ https://example.com/dl
A relative path the format check rejects
A path with a space and a query string does not match the safe regex ^[./\w-]+(\.\w+)?(#[\w-]+)?$, so it is marked with ?. The tool cannot tell you whether the file exists — only that the path shape is risky.
Input: Broken layout: [old page](./my docs/page.html?ref=nav) Report: # Link Validation Report Checked 0 external URLs and 1 relative links. > Note: Due to browser CORS restrictions, only DNS/network reachability can be tested, not HTTP status codes. ## Relative Links (format check only) ? ./my docs/page.html?ref=nav
Reference-style links are invisible to the validator
Links written in reference style match neither regex, so they never appear in the report. Convert them to inline first so they get checked.
Input: Read the [spec][1] before merging. [1]: https://example.com/spec Report: # Link Validation Report Checked 0 external URLs and 0 relative links. > Note: Due to browser CORS restrictions, only DNS/network reachability can be tested, not HTTP status codes. Fix: run md-ref-link-converter (to-inline) first, then re-validate.
More than 50 external URLs
A link-heavy resource page. Only the first 50 unique external URLs are probed; the remainder are acknowledged but not tested in this run.
Input: a curated list with 88 unique https:// links. Report (tail): ## External URLs ✓ https://example.com/1 ✓ https://example.com/2 ... ✗ https://example.com/50 (unreachable or CORS-blocked) ... and 38 more Tip: split the page with md-splitter and validate each section to cover all URLs.
Edge cases and what actually happens
HTTP 404 / 500 status codes are never detected
By designIn the browser the probe uses fetch(url, { mode: "no-cors" }), which returns an opaque response — the status code is unreadable. A page that returns 404 but whose server is reachable will show ✓. The validator detects network/DNS-level reachability, not HTTP status. For true 404 auditing, use a server-side crawler.
A live site blocks cross-origin requests
CORS-blockedMany production sites reject cross-origin fetch. When that happens the probe throws and the URL is reported ✗ (unreachable or CORS-blocked) even though the page is perfectly alive. Treat ✗ as a candidate to verify manually, not a confirmed dead link.
Only the first 50 external URLs are probed
CappedThe external-URL loop slices to urls.slice(0, 50). If a doc has more than 50 unique external URLs, the rest are summarised as ... and N more and are not network-tested in that run. Relative links are not capped. Split the doc with md-splitter to cover every URL.
Reference-style links are not extracted
Not checkedBoth regexes target bare URLs and inline [text](path) links. Reference-style links ([text][ref] plus a [ref]: url definition) match neither, so they are silently skipped. Convert them with md-ref-link-converter (to-inline mode) before validating.
Per-URL probe times out after 5 seconds
TimeoutEach browser probe is wrapped in an AbortController that fires after 5000 ms. A slow or hanging endpoint aborts and is reported ✗. The timeout is fixed and not configurable from the UI.
Anchor-only and bare-fragment paths
Format check onlyThe relative-path regex permits an optional trailing #anchor, so ./page.md#setup passes the format check. It does NOT confirm that a heading named setup exists — there is no heading-resolution step. Verify in-page anchors manually or in your site preview.
Protocol-relative URLs (//cdn.example.com)
SuspiciousA [logo](//cdn.example.com/x.png) link starts with //, not http(s), so it falls into the relative-link pass and fails the safe-format regex — reported ?. It is never network-tested. Rewrite to an absolute https:// URL if you want it probed.
Links inside fenced code blocks are still extracted
ExpectedThe regexes scan the raw text and do not exclude fenced code. A URL shown as a literal example inside ``` fences will be extracted and probed. If a code sample contains a deliberately fake URL, expect it to show ✗` in the report.
Email and other non-http(s) schemes
Not probedmailto:, tel:, and ftp: targets do not match the http(s) URL regex. If written as [email](mailto:x@y.com) they enter the relative-link pass and the format regex marks them ?. They are never network-tested.
Input exceeds the free tier limit
413 rejectedThe markdown family Free tier caps a run at 1 MB / 500,000 characters (the character cap is separate from byte size). Oversized input is rejected before processing. Pro raises this to 10 MB / 5,000,000 characters; larger docs need a higher tier or splitting first.
Frequently asked questions
Does the validator detect HTTP 404 errors?
No. In the browser it uses a no-cors fetch, which returns an opaque response with no readable status code. It detects network/DNS reachability, not HTTP status. A reachable server that returns 404 will still show ✓. For 404-level auditing, use a server-side crawler.
Why is a link I know is live reported with ✗?
The most common cause is CORS: the destination rejects cross-origin browser requests, so the probe throws and the link is marked ✗ (unreachable or CORS-blocked). The second cause is the 5-second per-URL timeout firing on a slow endpoint. Open the URL in a normal tab to confirm.
Are relative links actually checked against my filesystem?
No. Browser tools cannot read your local filesystem. Relative links are format-checked against the regex ^[./\w-]+(\.\w+)?(#[\w-]+)?$ only — ✓ means the shape is safe, ? means it looks risky. Confirm the file exists in your repo or dev preview.
Does it validate in-document anchors against my headings?
No. The format regex allows an optional #anchor suffix on a path, but the tool never builds a list of your headings or checks that the anchor resolves. Anchor existence must be verified manually or in a rendered preview.
Why does the count of external URLs look lower than expected?
Reference-style links ([text][ref]) are not extracted by either regex, so they are excluded from the count. Convert them to inline links with md-ref-link-converter before validating, then the URLs will be counted and probed.
Are duplicate links checked more than once?
No. Both the external-URL and relative-link lists are wrapped in a Set, so each unique value is checked exactly once. The count line reflects unique links, not raw occurrences.
How many external URLs will it actually test?
Up to 50 per run. The loop slices the unique external-URL list to the first 50; any beyond that are reported as ... and N more without being probed. Relative-path format checks are not capped.
What file types and how many files can I drop?
One Markdown file per run (.md, .mdx, .markdown), or pasted text. The tool does not accept multiple files. To audit several docs at once, merge them first with md-merger.
What is the output, and can I get HTML?
The output is a plain-text report (# Link Validation Report) downloaded as <name>-link-report.txt. It is not rendered HTML or annotated Markdown. If you need HTML elsewhere, that is a different tool — see md-to-html.
Does it follow redirects?
The browser fetch may follow redirects internally, but because the response is opaque under no-cors, the validator can only tell you the request did not throw — not where it landed or what status it returned. Do not rely on it to confirm a final destination.
Is my document uploaded anywhere?
The validation runs in your browser. External-URL probes do make outbound requests from your browser to those URLs (that is how reachability is tested), but the Markdown text itself is parsed locally and not uploaded to a JAD server.
Can I exclude certain domains or change the timeout?
No. The validator has no options — it is a single-action tool with a fixed 5-second timeout and a fixed 50-URL cap. There is no domain allowlist, denylist, or timeout setting in the UI.
Privacy first
All Markdown processing runs locally in your browser using JavaScript. No file is ever uploaded to JAD Apps servers — only metadata counters are saved for signed-in dashboard stats.