pickuma.
Meta

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.

9 min read

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.

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.ts
const { 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.

ApproachFixing a dead linkDetects rotBest for
Hardcoded URLs in articlesGrep across every file, edit, redeployOnly when a reader complainsA handful of articles you can audit by hand
Browser-extension or SaaS link checkerStill have to edit each articleYes, but reports against live pagesSites that can’t change their stack
Redirect table + status sweep (ours)One-row UPDATE, no article editsWeekly automated, human-reviewedEditorial 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?+
A 404 says 'this never existed,' which isn't true for a link you deliberately pulled. A 410 Gone tells crawlers and browsers the resource was intentionally removed, which is honest and helps search engines de-index it faster. It also lets us keep the slug's click history intact instead of hard-deleting the row, so we don't lose the audit trail of what that slug used to point at.
Doesn't a redirect hop hurt SEO or click-through?+
Outbound affiliate links generally shouldn't pass SEO equity anyway — they're typically marked nofollow or sponsored. The extra hop is a fast server-side 301 that's invisible to readers, and the upside is enormous: you can swap a target without touching a single article. For affiliate links specifically, the indirection is a clear net win.
How often should the link sweep run?+
Weekly has been the right cadence for us — frequent enough to catch rot within days, infrequent enough not to look like an attack to the sites you're checking. The right interval depends on how volatile your niche is; fast-moving dev tooling rots faster than established enterprise products. The bigger rule is to treat the output as a human worklist, not an autopilot that pauses links on a single failure.
Can the automated sweep catch a tool that died but whose homepage still loads?+
No, and that's the key limitation. A parked domain or an 'acquired by' splash page returns a healthy 200, so a status check sees nothing wrong. Those zombie links can only be caught by a human who knows the space. The sweep handles the mechanical failures so a person can spend their attention on the judgment calls.
What happens to revenue when I pause a dead affiliate link?+
You lose the click-through for that specific recommendation, which is real but smaller than the alternative cost: sending readers to a dead end erodes the trust that makes any of your links worth clicking. We'd rather pause a link and lose a few clicks than keep an insolvent tool live and lose a reader's confidence in the whole site. When there's a credible replacement, we repoint the slug instead of pausing, so the article keeps working with zero edits.

Related reading

See all Meta articles →

Get the best tools, weekly

One email every Friday. No spam, unsubscribe anytime.