How We Handle Internal Linking Across Hundreds of Articles Without a Spreadsheet
The internal linking system behind pickuma.com: a typed URL helper, an automated related-posts scorer, and a build step that fails when a link would 404.
Most blogs treat internal linking as a manual chore. Someone finishes an article, opens three older posts, and pastes a few <a href> tags by hand. That works at 30 articles. It quietly breaks at 530, because the failure mode isn’t “I forgot to add a link” — it’s “I added a link to a URL that no longer exists.”
We run pickuma.com with a few hundred published articles and no link spreadsheet. Internal linking is handled by three pieces of code instead: a typed URL helper that makes hand-written paths impossible to get wrong, a scoring function that picks related articles automatically, and a build step that refuses to ship if any post’s redirect is missing. None of these are clever. They’re just the boring version of a problem that usually gets solved with vigilance, which is the one resource that doesn’t scale.
One function owns every URL
The site splits content across four audience prefixes — /for-dev/, /for-pm/, /for-junior/, and /for-investor/. A single article slug doesn’t tell you its public URL on its own; you also need its audience frontmatter field. That’s exactly the kind of two-part rule a human gets right 95% of the time and wrong on the article that matters.
So no source file is allowed to write a post path directly. Every internal link routes through one helper, postUrl(post), which reads the post’s audience and returns the correct prefixed path. Components call postUrl(post); scripts that don’t have a full post object call postUrlFromParts(audience, slug). The rule is enforced by convention and review: a hardcoded /posts/<slug>/ or /for-dev/<slug>/ string in a template is treated as a bug, not a shortcut.
The payoff showed up when we migrated old /posts/<slug>/ URLs to audience prefixes. Because every link already went through the helper, the migration was a one-line change to the helper plus 301 redirects — not a find-and-replace across hundreds of MDX files. The articles never knew their own URLs, so they never had to be rewritten.
Related links are scored, not chosen
The “Related reading” block at the bottom of every article isn’t curated. It’s computed at build time by a small scoring function that ranks every other published post against the current one:
- Same category: +10
- Each shared tool tag: +5
- Tie-break: most recently published wins
The top five survive and get rendered. If scoring produces nothing — a genuinely orphaned topic with no category or tag siblings — it falls back to the most recent posts so the block is never empty. The current article is always excluded, which sounds obvious until you’ve seen a site recommend you the page you’re already on.
This is deliberately dumb. There’s no embedding model, no semantic similarity, no vector database. Category-plus-tag overlap is a weak signal individually, but across a few hundred articles it produces related links that are good enough to keep a reader moving, and it costs nothing to run on every build. The same data drives the ToolsMentioned footer, which renders automatically from each article’s tools frontmatter (with a category-level fallback when an article lists no tools), so the affiliate surfaces and the editorial links stay consistent without separate upkeep.
The honest tradeoff: a hand-picked link from an author who knows both articles will usually beat a scored one. We accept slightly worse individual recommendations in exchange for every article getting some relevant outbound links the moment it publishes, with zero marginal effort. At this volume, coverage beats precision.
Notion
Where we keep the topic map and tag taxonomy that the scoring function leans on — categories and tool tags only work as a linking signal if they're applied consistently, and that consistency starts in the planning doc, not the article.
Free for personal use; team plans from $10/user/mo
Affiliate link · We earn a commission at no cost to you.
The build won’t ship a broken link
The part that actually lets us sleep is the guardrail. When old /posts/ URLs were redirected to audience prefixes, every post needed a matching redirect entry, generated from its frontmatter. A missing redirect means a live 301 chain breaks and an indexed URL starts 404ing — the kind of regression you don’t notice until search traffic dips weeks later.
Rather than trust ourselves to remember, verify-redirects.ts runs as part of the build and fails it if any post is missing its /posts/<slug>/ → /for-<audience>/<slug>/ redirect. A broken internal link surface can’t reach production because the deploy never completes. The check is cheap, it runs every time, and it has caught exactly the mistakes a human reviewer skims past on a 40-file content PR.
This is the pattern under all three pieces: move correctness from “remember to do it” to “the system does it, or stops you.” The URL helper makes wrong paths unrepresentable. The scorer makes empty related-link blocks impossible. The build check makes a missing redirect a failed deploy instead of a silent 404.
None of this requires a CMS plugin or a link-management SaaS. It’s three small files in a static-site build. The reason it holds up at 530 articles is precisely that it’s small: there’s nothing to keep in sync, no second source of truth, and no manual step that a busy week can skip.
FAQ
Why not use a semantic/embedding model to pick related articles?+
How do you stop internal links from breaking when URLs change?+
Does automated internal linking actually help SEO?+
Related reading
2026-06-10
The E-E-A-T Signals We Actually Invest In (and the Ones We Skip)
E-E-A-T is not a meta tag you can set. Here is where an AI-assisted publication spends real effort on trust signals, and where we decided the effort is wasted.
2026-06-09
What Shipping 490 Articles Taught Us About Content Velocity
Lessons from running an automated editorial pipeline to 490 published reviews: where velocity actually breaks, and the checks that keep throughput from becoming a liability.
2026-06-09
How Our /go Affiliate Redirect and Click Tracking Actually Works
A walkthrough of the database-backed redirect, UTM tagging, and click logging behind every affiliate link on Pickuma — and why we never hardcode a vendor URL.
2026-06-09
How We Avoid Keyword Cannibalization Across 490 Reviews
When a blog grows past a few hundred posts, your own pages start fighting each other in search. Here's the workflow we use to detect and fix keyword cannibalization at scale.
2026-06-09
How We Keep 490 Published Reviews From Going Stale
A look at the systems we use to stop nearly 500 tool reviews from quietly rotting: staleness scoring, automated link checks, and visible changelogs.
Get the best tools, weekly
One email every Friday. No spam, unsubscribe anytime.