Most broken links aren't caused by a page being deleted. They're caused by a small mistake in one part of the URL: a relative path resolved from the wrong place, a space that wasn't encoded, a query string that got mangled by an email client.
If you understand the parts of a URL, you can usually look at a broken link and say which part broke in a few seconds. Here's the tour.
https://user@shop.example.com:8443/products/blue%20chair?color=navy&size=l#reviews
\___/ \__/ \______________/ \__/\__________________/ \________________/ \_____/
scheme user host port path query fragment
You can take any URL apart with the built-in URL class in JavaScript (in browsers and Node):
const u = new URL("https://shop.example.com:8443/products/blue%20chair?color=navy&size=l#reviews");
console.log(u.protocol); // "https:"
console.log(u.hostname); // "shop.example.com"
console.log(u.port); // "8443"
console.log(u.pathname); // "/products/blue%20chair"
console.log(u.searchParams.get("color")); // "navy"
console.log(u.hash); // "#reviews"
Let's go part by part.
Scheme: https:
Tells the browser how to fetch: https, http, mailto, tel, ftp.
How it breaks links:
-
Missing scheme.
href="www.example.com"isn't a link to example.com. It's a relative link to a path calledwww.example.comon your own site. This is one of the most common broken links in CMS content. -
httpthat doesn't redirect. If the target site dropped plain HTTP,http://links fail even though the page exists.
// what a missing scheme actually resolves to
new URL("www.example.com", "https://myblog.dev/posts/hello/").href
// → "https://myblog.dev/posts/hello/www.example.com" (broken!)
Host: shop.example.com
The domain name (or IP address) of the server.
How it breaks links:
- Domain expired or changed. The whole site is gone, or moved to a new domain without redirects. Every link to it breaks at once.
-
Subdomain removed.
blog.example.comgot folded intoexample.com/blogand nobody set up redirects. -
Typos.
exmaple.com. Some of those typo domains are even registered by someone else.
Port: :8443
Usually invisible (443 for HTTPS, 80 for HTTP). When you see an explicit port in a public link, be suspicious.
How it breaks links: URLs copied from a staging or development environment, like http://localhost:3000/pricing or https://example.com:8443/admin, get pasted into content and published. They work for whoever wrote them and nobody else.
Path: /products/blue%20chair
The location of the resource on the server. This is where most link rot happens.
How it breaks links:
- Pages moved or renamed without redirects (the classic).
-
Case sensitivity. On many servers,
/Products/and/products/are different paths. A link that worked on a case-insensitive development machine can 404 in production. - Relative paths resolved from the wrong place. This is the subtle one:
const base = "https://example.com/docs/guide/"; // note the trailing slash
new URL("setup", base).href; // "https://example.com/docs/guide/setup"
new URL("../setup", base).href; // "https://example.com/docs/setup"
new URL("/setup", base).href; // "https://example.com/setup"
const base2 = "https://example.com/docs/guide"; // no trailing slash
new URL("setup", base2).href; // "https://example.com/docs/setup" (different!)
The same relative link resolves to different URLs depending on whether the current page ends with a slash. Move a page from /guide/ to /guide, and every relative link on it can quietly shift.
Encoding: %20 and friends
URLs can only contain a limited set of characters. Spaces, many symbols and non-ASCII characters must be percent-encoded: a space becomes %20, é becomes %C3%A9.
How it breaks links:
-
Unencoded spaces in file names (
/files/Annual Report.pdf). Some tools encode them, some cut the link off at the space. -
Double encoding.
%20gets encoded again to%2520, which points somewhere that doesn't exist.
encodeURI("https://example.com/files/Annual Report.pdf");
// → "https://example.com/files/Annual%20Report.pdf"
encodeURI("https://example.com/files/Annual%20Report.pdf");
// → "https://example.com/files/Annual%2520Report.pdf" (double-encoded: broken)
The fix is boring but effective: avoid spaces and special characters in file names and slugs entirely.
Query: ?color=navy&size=l
Parameters for the server: search terms, filters, tracking tags.
How it breaks links:
-
The server ignores or rejects parameters it doesn't expect after a redesign. A link to
?id=123stops working when the new site uses/item/123. -
&turned into&and back. In HTML source,&should be written as&. Copying between HTML, Markdown and plain text sometimes leaves a literal&in the URL, so the server sees a parameter calledamp;size. - Tracking parameters making "different" URLs for the same page. That's not broken, but it confuses analytics and deduplication.
Fragment: #reviews
Points to a spot within the page. Here's the key fact: the fragment is never sent to the server. The browser handles it after the page loads.
How it breaks links: if the target heading's id changes, the page still loads with a 200, but the browser lands at the top instead of the section. No status code reports it. A link checker has to load the page and look for the id to catch it.
Diagnosing a broken link, part by part
When a link fails, walk it left to right:
-
Scheme: present?
https? -
Host: does the domain still resolve? (
dig +short example.com) - Port: any explicit port that looks like dev or staging?
- Path: right case? Moved? Resolved relative to the right base?
-
Encoding: spaces,
%25, odd characters? -
Query:
&, or parameters the new site ignores? -
Fragment: does the anchor
idstill exist?
Most broken links give themselves away by step 4.
Automated checkers do the same walk at scale. A simple script with new URL() catches missing schemes and localhost links, and a crawler such as linkbruno.com checks the rest against live responses.
Related reading
- Should your docs link to GitHub's main branch or to a permalink? Both options rot.
- HEAD vs GET for link checking: HEAD is faster, and it lies more than you'd think
- Anchor link not working? How to find and fix broken #section links
- How to check a website for broken links for free (5 methods that actually work)
Which part of the URL has caused your most annoying broken link? My vote is the trailing-slash relative-path trap. Tell me yours in the comments.
Top comments (0)