Skip to content
crawlforgeEspañol
It broke8 August 202610 min readLeer en español

A link to someone else's site wasn't a link to someone else's site

We audited our own website and got twelve warnings about internal links marked nofollow. None of those links were internal. Underneath was a fault that turned any link to another domain's home page into a link to ours.

Yesterday we added share buttons to the foot of every devlog entry. Nothing exotic: six networks, six AI assistants, each with the article’s URL folded into its own parameter. Then, out of habit, we ran the tool over the built site before calling the change done.

Twelve new findings. INDEX-NOFOLLOW-INTERNAL, medium severity, once per devlog entry. The rule reports an internal link carrying nofollow — a way of asking a search engine not to follow a link from your own site to your own site. It’s usually an oversight, which is why the rule exists.

Trouble is, none of those links were internal. The thirteen we added point at twitter.com, linkedin.com, chatgpt.com, grok.com. Every one carries rel="nofollow noopener", which is precisely what belongs on a share button. The rule was flagging something that wasn’t there.

Four out of thirteen

First question: which ones. Of the thirteen links in the block, the tool only complained about four:

https://chatgpt.com/?q=…            ← flagged
https://www.perplexity.ai/?q=…      ← flagged
https://grok.com/?q=…               ← flagged
mailto:?subject=…                   ← flagged

https://claude.ai/new?q=…           ← fine
https://chat.mistral.ai/chat?q=…    ← fine
https://twitter.com/intent/tweet?…  ← fine
https://www.google.com/search?…     ← fine

It takes a moment to see. The four flagged ones have an empty path: the question mark comes straight after the domain. The eight that behaved have a path in between — /new, /chat, /intent/tweet, /search.

That’s enough to write a minimum case, which is what separates a hunch from a fault. Five links on a test page, five ways of pointing off-site, and then ask the tool where it thinks they go:

Link as written in the HTML Where it resolved it
https://example-a.com/?q=1 https://ours.local/
https://example-b.com?q=1 https://ours.local/
https://example-c.com/path?q=1 correct
https://example-d.com/ https://ours.local/
https://example-e.com/?utm_source=newsletter https://ours.local/

Four out of five. And the fourth is the one that stings: https://example-d.com/, an ordinary link to another site’s home page, was being recorded as a link to our own home page. The fifth is worse, because a trailing ?utm_source= is what half the links leaving a newsletter carry.

Why

The mode that audits a built folder has a function that maps URLs to files. It exists because a static file server hands /about, /about/ and /about/index.html from the same place, and without unifying them the tool audits one page three times and fires duplicate-content rules it invented itself.

That function looked at exactly one thing: the URL’s path. It took url.path(), found the matching file inside the folder, and returned that file’s published URL. It never asked about the host.

So https://chatgpt.com/?q=… went in, the function kept the path /, found index.html at the root of our dist/, and handed back the audited site’s home page. The domain evaporated on the way through.

Nor is the collision limited to a bare slash. Any path that exists inside the folder behaves the same way: if you have dist/blog/index.html and someone links to https://elsewhere.example/blog/, that link becomes yours.

What went missing

The false finding was the cheap part. A consultant who sees twelve odd findings looks at them, works out they’re share buttons, and gets on with the job.

The expensive part was on the other side. Those external links vanished from the crawl graph: if the tool believes a link points at your home page, it will never ask the other server for a status, and the rule that reports broken external links cannot see what isn’t there. On a site with a hundred outbound links to other domains’ home pages, that’s a hundred links nobody checked and no warning that they weren’t.

In exchange, those same links inflated the home page’s inbound link count — a signal other rules read when deciding whether a page is well connected.

The audit itself produced the number. Before the fix, the built site came back with 133 URLs. After, 171. Those 38 are the external links the tool was swallowing on a thirty-one-page website.

The fix, and the uncomfortable part

One comparison. In built-folder mode, if the URL’s origin doesn’t match the audited site’s, that URL isn’t a file here and there’s nothing to translate.

We wrote two regression tests, and before accepting them we reverted the fix to watch them fail. Both went red with the message they were supposed to give, and only then did the line go back in. A test you have never seen fail is a test you don’t know checks anything.

Now the uncomfortable part. This is the third time this project has decided something by reading the text of a path or a name instead of looking at where it actually points, and the second time in a month. The previous two are written up in the 4 August review: a network screen comparing host names rather than resolved addresses, and a perimeter deciding from the text of a host instead of the address being dialled.

Three instances of the same reasoning error, in three different places in the code, written by the same head. We’ve turned it into a standing review question: does this comparison read the text, or does it look at where the thing points?

Which release carries it

0.8.0, and it’s a minor rather than a patch for a reason that might reach you: a rule stops firing. If you had a CI check counting INDEX-NOFOLLOW-INTERNAL findings, your number is about to drop. You haven’t fixed anything; you were counting smoke.

The rest of the story is the usual one around here. One thousand and twenty-six tests green, fifty-nine rules each with its own test case, and the fault was found by someone looking at a real website with the tool pointed at it. No unit test was going to catch it, because the rule’s logic was correct — the defect lived in the seam between the engine and the rule.

Auditing your own site with your own product isn’t a nice demo for the home page. It’s the only way we’ve found to make these things surface early rather than late.

Built in the open

Every two weeks: measurements, defects, worked examples. Nothing else.

← Back to the devlog