TL;DR: Internal links are the only ranking input you fully control. Structure them in floors (homepage to hubs, hubs to clusters, posts sideways and down to location pages), route every URL through one helper function, and keep anchors specific and capped. On my own site, links from posts down to city pages moved those pages more than anything done on the pages themselves.
Backlinks depend on other people. Rankings depend on an algorithm you don't run. Even your content depends on what searchers happen to want this year. Internal links are the one ranking input where you hold every card: which pages link where, with what anchor text, from what context. And on most sites I audit, this lever is just... unpulled. A nav bar, a footer, and a "related posts" widget picking three random articles. That's it.
Here's the structure I've settled on after doing this properly on my own site, and what I'd skip if I were starting again.
Think in floors, not in pages
The mental model that made this click for me is a building. The homepage and main service pages are the top floor. Topic hubs sit under them. Individual posts and location pages are the ground floor. Authority flows down through links; relevance signals flow back up.
Every page should know its floor and link accordingly:
The homepage links down to hubs and key service pages. Hubs link down to every page in their cluster and up to the relevant service. Posts link sideways to sibling posts and down to the most specific pages on the site, which for a local business means city or location pages.
That last one surprised me in practice. Blog posts accumulate most of a site's inbound authority over time, and by default that authority dead-ends there. Adding a small, consistent block of links from each post down to location pages moved those pages more than anything I'd done to the location pages themselves.
One source of truth for URLs
This is a web development point disguised as an SEO point. If your internal links are handwritten strings scattered across templates, they will rot. A route gets renamed, a locale prefix changes, and now a third of your internal links 301 or 404 and nobody notices for months.
Build one function or one module that owns URL construction, and make every internal link on the site go through it. When I moved my link blocks to a single pathFor() style helper, two things happened: a class of silent bugs disappeared, and adding links became cheap enough that I actually did it. The discipline matters more than the implementation. A links.ts file, a Django reverse(), a Rails path helper, whatever your stack gives you. The rule is that no template ever concatenates a URL by hand.
Anchor text: specific, boring, varied
The anchor is the label the crawler reads. "Click here" wastes it. "Best affordable web design services in the greater metro area" overcooks it and looks like what it is.
What works: the target page's actual topic, phrased the way a person would say it, with natural variation across the site. If ten pages link to your pricing page, they shouldn't all use the identical string. And if you run a bilingual site, the anchors must match the target's language exactly. A Greek page linked with an English anchor is a small mismatch signal that adds up across hundreds of links.
Don't link everything to everything
The tempting failure mode, once you build the tooling, is spraying links. Fifty city links in every footer. Every post linking to every other post. This dilutes the signal you were trying to concentrate, and it turns your pages into link directories.
Two constraints keep it honest. First, cap the blocks: a handful of related posts, a handful of city links, chosen deterministically so the same page always shows the same links. Second, stride the selection. If every post links to the same seven cities, cities eight through seventeen get nothing. Rotate the window based on something stable, like the post's position in the archive, so coverage spreads across the whole cluster without any page carrying dozens of links.
Check it in the browser, not just the build
Internal linking changes pass type checks and tests while being wrong in ways only rendering reveals: a link block that renders empty on one locale, an anchor that falls back to English, an exclusion rule that fails so a post links to itself. After any linking change I load a real post in both languages and count the links. Two minutes. It has caught something real more than once.
None of this is glamorous. There's no tool to buy and no trick that works for a week before an update kills it. It's plumbing. But it's plumbing where you own every pipe, and that's rare enough in SEO to be worth taking seriously.
Key takeaways
- Think in floors: homepage links to hubs, hubs to their clusters, posts sideways to siblings and down to location pages. Authority flows down, relevance flows back up.
- Blog posts accumulate most inbound authority; without downward links it dead-ends there.
- One helper owns every internal URL. No template ever concatenates a link by hand.
- Anchor text: the target's actual topic, phrased naturally, varied across pages, always in the target's language.
- Cap and rotate link blocks deterministically so the whole cluster gets covered without any page becoming a link directory.
- Verify in the browser in both languages; linking bugs pass builds and fail renders.
