pickuma.
Meta

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.

7 min read

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.

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

Try Notion

Affiliate link · We earn a commission at no cost to you.

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?+
We tested the idea and decided against it for this use case. Category-plus-tag scoring runs at build time with no external service, no API cost, and no latency, and at a few hundred articles it produces related links that are good enough to lower bounce rate. An embedding pipeline adds infrastructure and a failure mode for a quality gain readers barely notice. We may revisit it past a few thousand articles, where tag overlap gets noisier.
How do you stop internal links from breaking when URLs change?+
No template writes a post path directly — every internal link goes through one helper that derives the URL from the article's audience field. Changing the URL scheme is a one-line change in the helper plus redirects, not an edit across every article. A build-time check then fails the deploy if any post is missing its redirect, so a broken link can't reach production.
Does automated internal linking actually help SEO?+
Indirectly. The related-posts block exists to raise pageviews per session and lower bounce rate, both of which are signals Google reads. The bigger win is structural: automatic links mean every new article is reachable from related older ones the day it publishes, so crawlers find it without waiting for someone to manually link it in.

Related reading

See all Meta articles →

Get the best tools, weekly

One email every Friday. No spam, unsubscribe anytime.