Design a strategic internal linking structure

Optimized navigation, strategic linking and automated links.

Railway lines converging in a large junction

Internal linking is the part of a programmatic site people generate last and should design first. Your template decides, for every page at once, which URLs Google can reach, which ones look important and where a visitor goes next. A bad rule gets copied to every page, and so does a good one.

Throughout this chapter, say you run a job board with 40 job categories and 500 French cities. That gives 20,000 possible pages like “Nursing jobs in Lyon”, and no human will ever place those links by hand.

The first job is discovery. A sitemap tells Google a URL exists, but a page with no incoming link is an orphan: it has no context, no path for a crawler and very little reason to be prioritized. On a large site, orphans are often the pages that sit at “Discovered - currently not indexed” for months.

The second job is distributing PageRank. Your homepage, your guides and your best hub pages collect most of the backlinks. Internal links are how that authority reaches the long tail. If a “Best nursing employers” guide earns links from the press, it should link down to the city pages it mentions.

The third job is moving people. A visitor on “Nursing jobs in Lyon” with too few openings wants Villeurbanne next door, or care assistant roles in Lyon. A SaaS with use case pages has the same logic pointing elsewhere: trial, customer proof, the detailed tutorial. Links are where your page stops being a dead end, and they should lead somewhere a visitor would actually choose to go.

I always draw three levels before writing a single link rule. Each one has a different job.

Level Job board example Volume What it does
Pillar Nursing jobs in France 40 Targets the broad query, collects links from everywhere below
Secondary Nursing jobs in Brittany 40 × 13 regions Groups cities, lists all of them
Detail Nursing jobs in Rennes up to 20,000 Targets the precise query, carries the real listings

With this shape, every city page sits three clicks from the homepage: home, pillar, region, city. That only holds if each region page lists its cities in full, in plain links. A “top 10 cities” widget leaves the other 490 stranded.

Links then run in three directions. Up, through the breadcrumb. Down, through the lists on pillars and regions. Sideways, between pages that share a job or a place, and this last direction is where programmatic sites have an edge because the data already knows what is related.

Aerial view of tracks crossing a town
Block Where Rule from the data Sample anchor
Breadcrumb Top of page Parent category, parent region Nursing jobs in Brittany
Nearby cities After the listings Same job, 5 closest cities with open roles Nursing jobs in Vannes
Related jobs After the listings Same city, 5 closest job categories Care assistant jobs in Rennes
Contextual link Inside the intro One link to the most relevant guide what a nurse earns in Brittany
Conversion Sidebar or end of page Same link everywhere, wording varies by job post a nursing job

The contextual link matters more than people expect. A link sitting in a sentence carries more meaning than one in a list, so write at least one into the template’s editorial text, with the target picked from the data.

Treat links as data. I compute them into a table rather than improvising them in the template, because a table can be audited, diffed and rolled back. Here is the sideways block in PostgreSQL, assuming a pages table and a city_neighbors table with distances:

INSERT INTO page_links (source_id, target_id, block)
SELECT p.id, t.id, 'nearby_cities'
FROM pages p
JOIN LATERAL (
  SELECT target.id
  FROM city_neighbors cn
  JOIN pages target
    ON target.city_id = cn.neighbor_id
   AND target.job_id = p.job_id
  WHERE cn.city_id = p.city_id
    AND target.published
    AND target.open_roles > 0
  ORDER BY cn.distance_km
  LIMIT 5
) t ON true
WHERE p.published;

The two filters inside the subquery do the important work. Never link to an unpublished page, and never link to an empty one. A page with zero openings should not be published at all: Google’s scaled content abuse policy targets mass pages with little value, and your link graph is a very direct statement of which pages you consider worth visiting.

If you want to see this as a production variable, internal linking was one of the 90+ variables on the Deux.io project, alongside titles, schemas and featured articles. The structure of the table depends on your model, which the database chapter covers.

Vary anchors, but keep them stable

Thousands of identical anchors pointing at one page look mechanical and describe less than they could. Write a few patterns and assign one per source page.

Pattern Output
{job} jobs in {city} Nursing jobs in Vannes
{job} roles around {city} Nursing roles around Vannes
Work in {job_lower} in {city} Work in nursing in Vannes

Pick the pattern with something deterministic such as source_id % 3. If you use a random draw, every build rewrites thousands of anchors for no reason, and you lose the ability to tell what changed when rankings move.

Railway line cutting through forest, from above

Output HTML a crawler can follow

Google follows <a> elements with an href. Its link documentation is short and worth reading once. Render these blocks server side, in the initial HTML.

<nav aria-label="Nearby cities">
  <h2>Nursing jobs near Rennes</h2>
  <ul>
    <li><a href="/jobs/nursing/vannes">Nursing jobs in Vannes</a></li>
    <li><a href="/jobs/nursing/saint-malo">Nursing roles around Saint-Malo</a></li>
  </ul>
</nav>

A <span onclick>, a button that loads more results, or a block injected after a user interaction will not do this job. Leave nofollow out of your internal links too, since it asks Google not to rely on your own link and gives you no useful control over PageRank. The crawlers feeding AI answers need the same plain HTML paths, and many of them execute less JavaScript than Googlebot does.

The breadcrumb deserves its visible links plus its BreadcrumbList markup, which the structured data chapter walks through.

Audit the graph on a schedule

Because links live in a table, finding orphans is one query:

SELECT p.slug
FROM pages p
LEFT JOIN page_links l ON l.target_id = p.id
WHERE p.published
  AND l.target_id IS NULL;

Then compare with reality. Crawl the site with Screaming Frog from the homepage and diff the crawled URLs against your published list: any gap means the HTML does not match the table. In Search Console, the Links report shows which pages get the most internal links, and that list should look like your hierarchy. The Page indexing report shows whether the detail pages are stuck at the discovery stage. I cover running these checks automatically in the SEO audits chapter.

Your next step: draw the three levels for your own site on one page, write the rule behind each link block in one sentence, and check that every rule can be answered by a column you already have. Any block that needs a column you lack goes on the data model to-do list before the template gets built.