Auditing Affiliate Link Rot at Scale: How We Keep 200+ Outbound Links Honest
Affiliate links rot quietly — programs close, slugs change, tools die. Here's the redirect-table architecture and automated HTTP sweep we use to keep 200+ outbound links on Pickuma pointing somewhere real.
The first time I noticed link rot on this site, it wasn’t from a monitoring alert. It was a reader emailing to say a “best CLI for X” recommendation pointed at a 404, and that the tool had apparently been acquired and sunset months earlier. I went looking and found three more dead links in the same neighborhood. None of them had thrown an error, none had shown up in any dashboard. They’d just quietly stopped going anywhere useful while the articles kept ranking and kept sending people there.
That’s the thing about outbound links: they fail silently and they fail on someone else’s schedule. You can write a perfectly accurate article in March, and by September a third of its recommendations can be pointing at parked domains, expired affiliate programs, or rebranded products. The content didn’t change. The internet underneath it did. When you’re running a couple hundred articles, each with several outbound and affiliate links, “I’ll just check them by hand” stops being a plan and starts being a fantasy. This is the system we ended up building to keep all of it honest, and the editorial calls that the technical part can’t make for you.
Why affiliate links rot in the first place
It helps to be specific about the failure modes, because each one needs a different response. In my experience the rot breaks down into three buckets.
The first is program death. An affiliate program shuts down, changes networks, or quietly stops paying. The tool still exists and the link still resolves — it just no longer earns anything, or it now redirects through a tracking domain that’s been deprecated. These are the sneakiest because the page still returns a 200. Nothing is “broken” in an HTTP sense; the relationship behind the link is what died.
The second is URL drift. A company restructures its docs, moves pricing to a new path, renames a product, or migrates domains. The classic version is a tool that moves from tool.io/pricing to tool.com/plans and sets up redirects for exactly six months before letting them lapse. Your deep link rots even though the company is alive and well.
The third, and the one that hits an editorial site hardest, is tool death. The project gets abandoned, the startup runs out of runway, the repo gets archived. Now you’ve got a glowing recommendation pointing at a product nobody can buy or run. This isn’t just a broken link — it’s a credibility problem. A dead 404 looks careless. A live, enthusiastic recommendation for an insolvent company looks like you’re not paying attention to the thing you claim to cover.
The redirect indirection: never hardcode a target
The single best decision we made was to stop putting affiliate URLs in article bodies at all. Not one. Every outbound affiliate link on the site is written as an internal path — /go/[slug] — and the real destination lives somewhere else entirely.
So instead of an author writing a raw tracking URL into MDX, they write something closer to /go/some-tool. When a reader clicks it, an Astro route resolves that slug against our database, records the click, and issues a redirect to wherever the slug currently points. The article never knows or cares what the actual destination is. It only knows the slug.
The payoff is that the article body and the link target become completely decoupled. The day a tool gets acquired and its deep link breaks, I don’t open a single .mdx file. I don’t run a grep across the content directory hoping I catch every variant of the URL. I change one row in one table, and every article that references that slug — whether it’s one article or forty — instantly points at the new target. If the program is gone and there’s no good replacement, I flip the slug’s status and the link stops sending people to a dead end.
That last part matters more than it sounds. Without indirection, “this tool died” is a content migration: find every mention, edit every file, redeploy, hope you didn’t miss one buried in a comparison table. With indirection, it’s an UPDATE. The cost of doing the right thing drops low enough that you actually do it.
Supabase as the source of truth
The table behind all of this is deliberately boring. It lives in Supabase, it’s called affiliate_links, and the columns that matter are slug, target_url, and status. The slug is the stable handle the articles reference. The target URL is the current real destination. The status is what lets us turn a link off without deleting its history.
The resolution logic is small enough that there’s nothing clever to hide. The /go/[slug] route looks the slug up, and the behavior keys off status:
// simplified from src/lib/redirect.tsconst { data } = await client .from('affiliate_links') .select('slug, target_url, status') .eq('slug', slug) .maybeSingle();
if (!data) return { status: 404 };if (data.status !== 'active') return { status: 410 };return { status: 301, targetUrl: data.target_url };There are three outcomes, and each one is chosen on purpose. An unknown slug is a 404 — that’s a genuine “this never existed” case, usually a typo in a draft. A known slug whose status isn’t active returns a 410 Gone, not a redirect and not a 404. And an active slug gets a 301 to its current target.
The 410 is the detail I’d push anyone to copy. When we deliberately retire a link — the program closed, the tool died, we pulled the recommendation — we set status to paused rather than leaving it pointing at a stale URL or hard-deleting the row. A 410 Gone tells crawlers and browsers that this resource is intentionally gone, which is honest, and it keeps the click-history row intact so we don’t lose the record of what that slug used to be. Deleting would erase the audit trail. Pointing it at a dead URL would be lying. Pausing is the truthful middle.
Keeping the destinations in one queryable table also means the whole link graph is inspectable. I can ask “which slugs are paused,” “which haven’t been touched in a year,” or “which target a domain that keeps showing up in failures” with a query instead of a crawl. The articles stay clean; the messy, fast-changing part lives in a database where messy, fast-changing data belongs.
The automated sweep that catches the rest
Indirection makes fixing rot cheap. It doesn’t tell you a link rotted. For that we run a periodic HTTP status sweep over every target_url in the table.
The sweep is intentionally simple. It pulls the active rows, requests each target, and records what came back. Anything that resolves cleanly is left alone. Anything that returns a 4xx or 5xx, times out, or lands on a redirect chain that ends somewhere suspicious gets flagged into a report I actually read. I don’t auto-pause on a single failure, because transient blips and aggressive bot-blocking produce false positives — a tool’s marketing site throwing a 403 at an unfamiliar user agent is not the same as that tool being dead. A link has to fail consistently before it earns a human look.
A few hard-won details. Use a real, identifiable user agent and a generous timeout; a lot of “failures” are just sites that don’t like terse default crawlers. Follow redirects but inspect where you land, because a 200 at the end of a chain that dumped you on a homepage is exactly the zombie case from earlier. And treat the sweep’s output as a worklist, not an autopilot. Its job is to shrink “audit 200 links” down to “look at the eight that misbehaved this week,” which is a task a human can finish over coffee.
What the sweep genuinely cannot do is judge whether a tool is still worth recommending. It can tell me a link is alive. It can’t tell me the company behind it laid off its team and stopped shipping. That judgment is the editorial layer, and it’s the part I refuse to automate away.
How this compares to the usual approaches
Most sites handle outbound links one of a few ways, and the differences come down to how expensive a fix is and whether anyone notices the rot before readers do.
| Approach | Fixing a dead link | Detects rot | Best for |
|---|---|---|---|
| Hardcoded URLs in articles | Grep across every file, edit, redeploy | Only when a reader complains | A handful of articles you can audit by hand |
| Browser-extension or SaaS link checker | Still have to edit each article | Yes, but reports against live pages | Sites that can’t change their stack |
| Redirect table + status sweep (ours) | One-row UPDATE, no article edits | Weekly automated, human-reviewed | Editorial sites with shared links across many posts |
A third-party link checker is a reasonable starting point and genuinely better than nothing — it’ll surface the 404s. But it solves detection without solving the fix. You still open every affected article. And it tends to be blind to the affiliate-specific failure where the page is fine but the program is dead. The redirect-table approach costs more up front — you have to build the route and the table — but it collapses the expensive half of the problem, the editing, down to nearly nothing.
Who should build this, and who shouldn’t
If you’ve got a dozen articles and a quiet niche, this is overkill. A spreadsheet and a quarterly manual pass will serve you fine, and you’ll spend your time better writing than building infrastructure. The indirection layer earns its keep specifically when the same link appears across many articles, when you’re running an affiliate program where a dead target is also lost revenue, or when your credibility is the product and a stale recommendation costs you more than a broken image would.
The break-even, in my experience, is somewhere around the point where you can no longer hold every outbound link in your head — for us that was a few dozen articles in. Below that, the manual approach is honestly more pragmatic. Above it, every month you delay building indirection is another month of rot accumulating in files you’ll eventually have to hand-edit anyway. If you’re already on a stack with a database and a redirect-capable router, the build is a weekend. If you’re on pure static hosting with no backend, the calculus shifts and a SaaS checker plus discipline might be the right call.
FAQ
FAQ
Why return 410 Gone instead of just 404 for a retired link?+
Doesn't a redirect hop hurt SEO or click-through?+
How often should the link sweep run?+
Can the automated sweep catch a tool that died but whose homepage still loads?+
What happens to revenue when I pause a dead affiliate link?+
Related reading
2026-06-04
The Economics of a 330-Article Blog: Hosting, Tooling, and Cost per Article
A transparent teardown of what it actually costs to run a 330+ article developer blog — near-zero edge hosting, low-tier tooling, LLM drafting spend, and the real marginal cost per article.
2026-06-04
GEO Is the New SEO: Optimizing Developer Content for AI Answer Engines in 2026
Ranking in blue links matters less when ChatGPT, Perplexity, and Google AI Overviews answer the question before a click happens. Here's what actually gets developer content cited by answer engines — and what's just SEO theater rebranded.
2026-06-04
The Programmatic SEO Trap: Why We Stopped Scaling Pickuma With Templates
We mapped out a programmatic SEO build for Pickuma — thousands of templated tool-comparison pages — and then killed it. Here's the math, the post-2024 ranking reality, and the heuristic we use now to tell legitimate pSEO from slop.
2026-06-04
What Launching pickuma-play Games Taught Us About Traffic and Audience
We spun up a browser-games hub next to this editorial blog. The audiences barely overlapped — and that gap taught us more about distribution than any single channel could on its own.
2026-05-28
AI-Assisted Writing Disclosure: Where We Draw the Line
Most 'AI-assisted' badges are vague. Here's the binary threshold we use for flagging articles, why FTC and E-E-A-T guidance pushed us there, and the edge cases that still leak.
Get the best tools, weekly
One email every Friday. No spam, unsubscribe anytime.