Guides · 2026-09-23 · By VSNARY | Emmanuel Orta · 0 views
How to find broken internal links and dead anchor links
A link audit is an inventory, a fetch, a classification and a second pass over in-page anchors. The classification is where most audits go wrong, and the anchors are where most audits never look.
Start from the sitemap, fetch every page, and collect every internal link on those pages. Request the targets the sitemap does not list and record each status. Classify by what the request was: a redirect is working but worth updating, a 400 on an API endpoint without parameters is correct, a 404 on a page link is broken. Then check that every in-page link has a matching id in the rendered page, and trace each defect to the template that generated it.
Most internal link audits are a crawl and a list of non-200 responses. That finds some broken links and reports others that are not broken, and it misses a whole class of dead links entirely: the ones that point at a place on the same page. This guide covers the full method, in the order we ran it on crawlcheck.io, with the pitfalls that produced wrong answers along the way.
Step 1: build the inventory from the sitemap #
The sitemap is the site's own statement of which pages exist. Fetch it, list every URL, and request each one. Every URL in a sitemap should answer 200 on its final hop; a sitemap that lists redirects or errors is itself the first finding. Checking the sitemap first also tells you whether it can be trusted as the inventory. The sitemap audit guide covers what to look for in the file itself.
Step 2: collect every internal link #
On each fetched page, collect every href that points inside the site: relative paths, and absolute URLs on your own host. Strip fragments and query strings for this pass, and de-duplicate. On crawlcheck.io, 924 pages produced 2,260 distinct targets. Many will already be in the sitemap. The ones that are not are the interesting part: pages the site links to but does not declare, and addresses that should not exist.
Collect from the served HTML, not from the source templates. Links assembled at render time, or included only for certain records, appear only in the output.
Step 3: request what the sitemap does not cover #
Request each target that is not in the sitemap, with a short timeout, and record the final status and whether it redirected. Run requests in parallel if the site can take it, but keep the count modest: a link audit that trips your own rate limit produces a list of 429s that are about the audit, not the links.
Step 4: tell broken links from correct errors #
A status code answers the request that was made. Before calling a link broken, ask what the request was.
| Answer | Usually means | Action |
|---|---|---|
| 200 | working | none |
| 301, 308 | moved permanently | update the link to the final address |
| 302, 307 | temporary redirect | check it is meant to be temporary |
| 400 on an API endpoint | request needs parameters | correct; exclude from the broken list |
| 401, 403 | login or firewall | check the page is meant to be public |
| 404, 410 on a page link | broken | fix the link or restore the page |
| 429 | the audit was rate-limited | slow down and re-request |
| 5xx | server error | re-request; persistent errors are defects |
On crawlcheck.io, six links answered 400. Every one was an API endpoint linked from documentation as an address, which needs a domain in the request to do anything. A 400 is the correct answer to an incomplete request. Counting them as broken would have tripled the defect count with no defect behind it.
Step 5: find broken anchor links (#fragments) in the rendered page #
A link to #section fetches nothing, so no status-based crawl will ever flag it. It works only if the same page contains an element whose id matches. Checking this means reading each rendered page, listing every link that starts with #, and confirming each target id exists.
Check the rendered document, not the source. Sections that are drawn only for some records, a local business section on a report for a site that is not a local business, for example, exist in the template but not on the page. On crawlcheck.io, one report carried 38 in-page anchors, and three pointed at sections that are only drawn for local businesses.
Query for any element with the id, not a specific kind of element. Our first check looked for a section element with a given id, reported it missing, and was wrong: the id was on a different element. A check shaped more narrowly than the question it claims to answer produces confident false positives.
Step 6: trace each defect to its template #
On a site built from templates, a broken link is rarely a one-off. It is an assumption in shared code that fails for some input. Our one broken page link came from a table that links every row to a directory profile, used on a page whose single row is deliberately not in the directory. The three dead anchors came from a list that linked every section, drawn or not. Fixing each at the template fixed every page of that kind at once.
So for each defect, find the code that produced it and ask which other pages use it. Then test a contrasting case: a page where the link should still work. After the anchor fix, a report for a local business still linked all three sections, and all three were there.
Step 7: decide what redirects to keep #
A redirected internal link works, but it costs every crawler an extra request and dilutes the signal of which address is canonical. Update internal links to point at final addresses. Keep the redirect for anyone arriving from outside. When you remove a page that was linked, redirect it to its closest replacement rather than letting it 404; the canonical URL guide covers keeping one address per page.
How often to run it #
Run the full audit after any change to shared templates, navigation or routing, because those are the changes that break links on many pages at once. Between those, a check of the sitemap and the pages it lists catches most regressions. The method needs nothing beyond a sitemap, an HTTP client and a way to read rendered HTML.
Every figure above came out of this scanner.
Point it at your own domain and see the same measurements, free.
The main product
Found this on your own site? We fix it for $749.
Scan free to see where you stand. The fix is one site, every finding implemented and re-measured, with a sealed before and after.
Questions this post answers
What is the best starting point for an internal link audit?
The sitemap. It is the site's own list of pages, so fetch every URL in it first, then collect the links on those pages.
Is a 400 response always a broken link?
No. An API endpoint linked from documentation will answer 400 when requested without its required parameters. That is the correct response to an incomplete request.
How do I find broken in-page anchors?
List every link starting with # on the rendered page and check that an element with that id exists on the same page. A status-code crawl cannot find these, because the link fetches nothing.
Should internal links point at redirects?
They work, but each costs an extra request. Update internal links to the final address and keep the redirect for visitors arriving from elsewhere.
Why trace a broken link back to its template?
On a templated site, one defect in shared code usually breaks the same link on every page built from it. Fixing the template fixes all of them.
Related findings
Comments
Comments are read before they appear. Nothing is published automatically, and no account is needed.
Writing about this? Facts, live figures and marks — every number on that page is dated and traceable to a scan.