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.
If you take one idea from this, take this one: centralize URL construction behind a single function before you have a URL scheme problem, not after. The cost is one helper file. The alternative cost is editing every article the day you change your routing — and quietly shipping a dozen broken links because you missed a few.
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.
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.
Automated related-links are a real SEO footgun if your category and tag taxonomy is sloppy. The scorer is only as good as the metadata: mislabel an article's category and it will confidently recommend five unrelated posts to every reader. Audit your taxonomy before you automate link generation on top of it — garbage tags produce garbage links at scale, faster than you can read them.
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.
Originally published at pickuma.com. Subscribe to the RSS or follow @pickuma.bsky.social for new reviews.
Top comments (0)