Last month I shipped a blog post on one of my sites that had 80 broken links. Not one or two. Eighty. I didn't notice for a week. The post was about SEO tooling and it referenced /tools/mega-analyzer/, /tools/analyzer/, /tools/link-graph/, and every other tool by relative URL. On the site where I originally wrote the post, every one of those links resolved. On the five sister sites where I copy-pasted the same markdown to republish, every single link 404'd.
It took three deploys to trace because the links looked correct in the source. The bug wasn't in any one link — the bug was an architectural mistake about where tools live versus where blog posts live in a multi-site network.
The Setup That Causes This
I run a network. One author, several content verticals, each on its own domain. There's a tools hub at jwatte.com/tools/* where every tool actually lives. There are content sites — book sites, service sites, satellite sites — that publish blog posts referencing those tools. When a blog post mentions a tool, the markdown gets written like any other internal link:
Check your site with the [Mega Analyzer](/tools/mega-analyzer/)
or run a full [Link Graph audit](/tools/link-graph/).
On the tools hub, both links resolve. On any other site in the network, both links 404. The tool URLs exist at jwatte.com/tools/*, not at othersite.com/tools/*. The relative path /tools/mega-analyzer/ means "from the current domain's root" and the current domain is whichever site is rendering the post.
This is a one-character mistake repeated 80 times in one post.
Why It's So Hard to Catch
Three reasons:
- The links look right. A reviewer scanning the markdown sees
/tools/mega-analyzer/and mentally compares it to the real URL structure, not to the current site's URL structure. The brain's link-check is "does this tool exist at this path" not "does this site have this path." - The build passes. Eleventy doesn't check external or internal link validity. The markdown renders to HTML, the HTML renders to a page, the anchor tags are syntactically valid.
- No error until click. The 404 only surfaces when someone (user or crawler) clicks the link. If the post is new and has no traffic yet, nobody reports the bug. By the time the first reader clicks, you've copied the same mistake across every site in the network.
The Fix: Absolute URLs for Anything Cross-Site
The rule I enforce now is simple. Anything that lives outside the current site's root gets an absolute URL. That means the full protocol, domain, and path. The tools hub content in a blog post on a satellite site looks like this:
Check your site with the [Mega Analyzer](https://jwatte.com/tools/mega-analyzer/)
or run a full [Link Graph audit](https://jwatte.com/tools/link-graph/).
Verbose? Yes. Portable? Also yes. The same markdown file can be published on any site in the network and the links still work. If I ever decide to mirror the tools hub to a CDN or change the primary domain, I update one place (see below), not 80 links across 40 posts.
Relative URLs are still correct for anything on the current site. A blog post linking to the About page of the same site should use /about/, not https://thissite.com/about/. The rule is scoped to cross-site references.
The Tools Registry Pattern
After the eighty-link mistake, I added a data/tools.json file to my Eleventy sites. It's a single source of truth for every tool in the network:
{
"mega-analyzer": {
"name": "Mega Analyzer",
"url": "https://jwatte.com/tools/mega-analyzer/",
"description": "Full SEO + AEO audit, 50+ signals."
},
"analyzer": {
"name": "Site Analyzer",
"url": "https://jwatte.com/tools/analyzer/",
"description": "Quick pass, essential checks."
},
"link-graph": {
"name": "Link Graph + Sitemap Delta",
"url": "https://jwatte.com/tools/link-graph/",
"description": "Find orphans and 404s on your site."
},
"internal-link-auditor": {
"name": "Internal Link Auditor",
"url": "https://jwatte.com/tools/internal-link-auditor/",
"description": "Auto-rewrite stale internal links."
},
"schema-completeness": {
"name": "Schema Completeness",
"url": "https://jwatte.com/tools/schema-completeness/",
"description": "Audit JSON-LD coverage per page type."
},
"voice-cleanup": {
"name": "Voice Cleanup",
"url": "https://jwatte.com/tools/voice-cleanup/",
"description": "Strip AI tells from drafts."
},
"slug-rename-helper": {
"name": "Slug Rename Helper",
"url": "https://jwatte.com/tools/slug-rename-helper/",
"description": "Rename slugs and update every inbound link."
}
}
And a paired Eleventy filter in .eleventy.js:
eleventyConfig.addFilter("tool", (slug) => {
const tools = require("./src/_data/tools.json");
const tool = tools[slug];
if (!tool) return `[missing tool: ${slug}]`;
return `<a href="${tool.url}">${tool.name}</a>`;
});
Then any blog post or page can reference a tool by slug:
{% raw %}
Check your site with {{ "mega-analyzer" | tool | safe }}
or run a full {{ "link-graph" | tool | safe }} audit.
{% endraw %}
If the tool URL ever changes, I edit tools.json once. Every site in the network pulls the updated URL on the next build. If I add a new tool, I add it to the registry and every site immediately has access to it by slug.
When I Skip the Registry
The registry is overkill for a single-site project. If you run one site, just use absolute URLs in markdown where cross-site links are needed (none, in that case — your tools are on the same domain). The registry only pays off once you have two or more sites that reference the same shared resource.
For the 27-site network I run, the registry has saved me easily 10 hours of link maintenance since I added it. The tools URLs haven't changed (yet), but I added four new tools this quarter and publishing them across the network was a single JSON edit.
The Detection Tool
The Internal Link Auditor at /tools/internal-link-auditor/ is the tool I wrote specifically for this bug class. You give it a site URL. It crawls the site, extracts every internal anchor, classifies each as:
- Same-site valid — relative or absolute link to a page that exists on this site.
- Same-site 404 — link to a path that doesn't resolve on this site.
- Cross-site relative — relative link to a path that doesn't exist on this site but is known to exist on a sister site in the registry. These are the eighty-link bugs.
- Cross-site valid — absolute link to a known sister site.
The third category is the one this whole post is about. It's the only category where the link "looks correct" on inspection but is definitely broken at runtime. The auditor will flag them and, if you want, output a patch that rewrites each relative URL to the absolute URL from the registry.
The tool is launching today on jwatte.com. The eighty-link post is what prompted it.
A Smaller Version of the Same Bug
One more variant worth mentioning. Even on a single site, the same class of bug shows up when you rename a section. Old markdown says /guides/foo/. You rename the section to /articles/. The old markdown still says /guides/foo/, which now 404s. The Internal Link Auditor catches this too: it's the "same-site 404" category, and the fix is the same rewrite pattern.
I hit this one when I merged two satellite sites last spring. Every old blog post that linked to the other site's posts suddenly had dead internal links. Took a weekend to patch by hand. The auditor would have done it in an hour.
The Short Version
- In a multi-site network, relative links like
/tools/foo/404 on every site except the one where the tool actually lives. This is the single biggest source of cross-site 404s I see. - The fix is absolute URLs for anything outside the current site's root. Use relative URLs only for in-site links.
- For any network with two or more sites that share resources, build a
data/tools.jsonregistry and a paired Eleventy filter. Reference by slug, not by URL. - The
/tools/internal-link-auditor/tool classifies every link on a site and can auto-rewrite cross-site relative links to absolute URLs from the registry. - Section renames cause the same bug class on a single site. The auditor catches those too.