Web

Internal linking that passes context

Take two sentences that point to the same page. The first says "Fonts are covered here." The second says "Swapping a fallback font without shifting the layout is covered in font loading without layout shift." Both are valid HTML. Only the second tells a reader, a screen reader's link list and a search engine what lies on the other side before anyone follows it.

What Google's documents commit to

The statements below come from Google's link best practices and SEO starter guide, and they are modest.

  • Google uses links as a signal for relevancy and to find new pages to crawl.
  • Anchor text, the visible text of a link, tells people and Google something about the destination. Good anchor text is descriptive, reasonably concise, and relevant both to the page it sits on and to the page it points to.
  • The words before and after a link matter, so the whole sentence deserves attention. Chains of adjacent links lose that surrounding text.
  • Every page you care about should have a link from at least one other page on your site.
  • There is no magical ideal number of links on a page.

The starter guide adds that the vast majority of new pages Google finds each day are found through links, and that organising a site logically helps, with a caution not to drop everything and reorganise, because search engines will likely understand your pages as they are right now. Nothing in these pages supports claims about how much any one link is worth, which link on a page counts most, or a target for links per page. This article makes none.

Anchor text: write for the reader scanning a list

Assistive technology can list every link on a page, and so can a reader's habit of scanning. WCAG 2.2 success criterion 2.4.4 (Level A) asks that the purpose of each link can be worked out from its text alone or from the text together with programmatically determined context. Its Understanding page states the intent plainly: meaningful link text helps people who choose from a list of links and people who tab from link to link, and it is a best practice for links with different purposes to have different text. MDN makes the same point from the other direction: the "list all links" shortcut emulates how sighted readers scan.

Lighthouse automates the crudest part of this check. Its audit flags a fixed set of generic phrases: "click here", "click this", "go", "here", "this", "start", "right here", "more" and "learn more". It also advises against using the page's URL as the link text without a good reason.

Weak anchor (fictional)ProblemBetter
Click here to learn moreSays nothing out of contextRead the audit routine for links
Read more (repeated on five cards)Five links, one labelName each article in its own link
https://example.com/journal/a/A URL describes nothingThe title or topic of the page
A full sentence of 25 words as the linkWeirdly long, per GoogleThe three or four words that name the destination
Five single-word links in a rowNo context around eachOne link per idea, with a sentence around it
responsive image sizes srcset guide tips bestForced keywordsThe words a person would use in speech

A good anchor still benefits from the sentence it sits in. Compare a paragraph with weak links and the same paragraph rewritten (fictional text, real destinations):

We wrote about images. Click here for more. We also covered fonts, here.

Choosing widths for a hero image is covered in images in 2026: AVIF, WebP and srcset. Once the image is sized, the next source of layout movement is usually text, which font loading without layout shift deals with.

The rewrite does three things. Each link names its destination. Each sentence says why the reader would follow the link, which is what Google means by giving context and setting expectations. And the two links are separated by explanatory text instead of sitting side by side.

Place a link where the reader needs the detail, at the first point the topic comes up in a way that would raise a question. A paragraph-ending dump of related links serves browsing, and that is a legitimate job for a "related articles" block, but it does not replace links within the text.

A link needs an <a> element with an href that resolves to a URL. Google's documentation lists what it can parse and what it cannot reliably extract:

  • Parsed: <a href="/products/category/shoes">, including with an extra onclick and class names.
  • Not recommended: <a onclick="goto('...')">, a <span href>, and javascript: URLs. Google may still attempt to parse them, but it does not promise to.

Links inserted by JavaScript are crawlable as long as the markup is a real anchor. Google advises using the URL Inspection tool to check that script-inserted anchor text is present in the rendered HTML. For images used as links, Google uses the alt text as the anchor text, and an empty alt inside an empty link leaves nothing to read.

MDN warns against anchors abused as buttons through href="#" or javascript:void(0). These break copying and dragging links, opening links in a new tab, bookmarking, and behaviour when scripts fail, and they send wrong semantics to screen readers. If the element performs an action instead of navigating, make it a <button>. The same reasoning is why how a page transition works without a framework keeps ordinary anchors underneath its script, and why static HTML first matters for links as much as for text.

Reachability: every page needs a way in

Google's rule that every page you care about should be linked from at least one other page has a mechanical test. The script below reads a folder of built HTML files, builds the internal link graph and reports six things: pages with no inbound link, pages that cannot be reached from the home page by following links, pages more than three clicks deep, generic anchors, links to pages that do not exist, and pages with no internal link inside <main>. Three clicks is an editorial yardstick chosen for this script. Google's pages give no depth threshold.

// link-audit.mjs   usage: node link-audit.mjs ./site-folder
import { readdirSync, readFileSync, statSync } from 'node:fs';
import { join, relative, sep } from 'node:path';

const root = process.argv[2];
const GENERIC = new Set(['click here', 'here', 'read more', 'learn more', 'more', 'this', 'this article', 'link', 'go', 'start']);
const MAX_CLICKS = 3; // an editorial yardstick, not a Google number

const walk = (dir) => readdirSync(dir).flatMap((n) => {
  const p = join(dir, n);
  return statSync(p).isDirectory() ? walk(p) : [p];
});
const routeOf = (file) => '/' + relative(root, file).split(sep).join('/').replace(/index\.html/, '');
const text = (html) => html.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim().toLowerCase();

const pages = new Map(); // route -> html
for (const f of walk(root).filter((f) => f.endsWith('.html'))) pages.set(routeOf(f), readFileSync(f, 'utf8'));

const edges = []; // { from, to, anchor, contextual }
for (const [from, html] of pages) {
  const main = (html.match(/<main[\s\S]*?<\/main>/i) || [''])[0];
  for (const m of html.matchAll(/<a\s[^>]*?href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/gi)) {
    const url = new URL(m[1], 'https://site.test' + from);
    if (url.host !== 'site.test') continue;
    edges.push({ from, to: url.pathname, anchor: text(m[2]), contextual: main.includes(m[0]) });
  }
}

const inbound = new Map([...pages.keys()].map((r) => [r, new Set()]));
const out = new Map([...pages.keys()].map((r) => [r, new Set()]));
for (const e of edges) {
  if (e.from === e.to) continue;
  if (inbound.has(e.to)) inbound.get(e.to).add(e.from);
  out.get(e.from).add(e.to);
}

// breadth-first search from the home page
const depth = new Map([['/', 0]]);
for (const queue = ['/']; queue.length;) {
  const r = queue.shift();
  for (const t of out.get(r) || []) if (pages.has(t) && !depth.has(t)) { depth.set(t, depth.get(r) + 1); queue.push(t); }
}

const line = (label, items) => console.log(label + ' (' + items.length + ')' + items.map((i) => '\n  ' + i).join(''));
line('No inbound link from another page', [...inbound].filter(([r, s]) => r !== '/' && s.size === 0).map(([r]) => r));
line('Not reachable from the home page by links', [...pages.keys()].filter((r) => !depth.has(r)));
line('More than ' + MAX_CLICKS + ' clicks from home', [...depth].filter(([, d]) => d > MAX_CLICKS).map(([r, d]) => r + '  (' + d + ')'));
line('Generic or empty anchor text', edges.filter((e) => e.contextual && (GENERIC.has(e.anchor) || e.anchor === '')).map((e) => e.from + ' -> ' + e.to + '  "' + e.anchor + '"'));
line('Links to pages that do not exist', edges.filter((e) => !pages.has(e.to)).map((e) => e.from + ' -> ' + e.to));
line('Pages with no internal link inside <main>', [...pages.keys()].filter((r) => !edges.some((e) => e.from === r && e.contextual)));

It was run against a fictional nine-page site. Every page has a menu linking to Home, About and Journal, and the journal articles a to f link to each other as follows: the index lists a and b; a links to b, to c with the words "click here", and to a page that was never built; b links back to a; c links to f; d links to e; nothing links to d. The output was:

No inbound link from another page (1)
  /journal/d/
Not reachable from the home page by links (2)
  /journal/d/
  /journal/e/
More than 3 clicks from home (1)
  /journal/f/  (4)
Generic or empty anchor text (1)
  /journal/a/ -> /journal/c/  "click here"
Links to pages that do not exist (1)
  /journal/a/ -> /journal/missing/
Pages with no internal link inside <main> (3)
  /about/
  /journal/e/
  /journal/f/

Page e shows why two lists exist. It has an inbound link, so it is not an orphan, yet its only source is itself unreachable. Each finding has an ordinary fix:

FindingFix
Orphan or unreachableLink to it from a page whose reader would want it, or decide it should not exist and remove it
Deep pageLink to it from a hub page or from the text of a closer page
Generic anchorRewrite the anchor and the sentence around it
Broken targetFix the link, or restore the page, or redirect the old URL
No contextual linksAdd the two or three destinations a reader would plausibly want next

The script has real limits. It uses regular expressions rather than an HTML parser, it reads built files and so ignores anything added by JavaScript, and it treats a link as contextual if its markup appears inside <main>. A sitemap lists orphans for discovery, as covered in sitemaps, robots.txt and Search Console, but a link also tells a reader why the page matters.

A routine for each new page

Links are cheapest to get right at the moment a page is published, because you know what it is for.

  1. Write the page's own links first, to the two or three existing pages a reader would want next, each inside a sentence that says why.
  2. Search the site for the topic and open the older pages that already discuss it. Where a reader of an older page would reasonably want the new one, edit a sentence there to link to it. A new page with no inbound link starts life as an orphan.
  3. Link to the final URL in the form the site uses, for example with the trailing slash, so readers and crawlers do not pass through a redirect on every visit. Canonical URLs, redirects and the www question explains why one consistent form matters.
  4. Run the audit and fix what it reports before the change goes live.
  5. When a page is renamed or removed, update the links that point to it. A redirect protects visitors with old bookmarks, and the links you control should still be corrected.

Google's spam policies treat hidden text or links as abuse when they exist solely to manipulate search engines. Their examples include white text on a white background, text positioned off-screen, a font size or opacity of 0, and a link attached to one small character in the middle of a paragraph. Accordions, tabs, tooltips and screen-reader-only text that improves the experience are listed as acceptable.

Checklist

  • Every link is an <a href> that goes to a real URL; actions are buttons.
  • Each anchor names its destination and still makes sense read alone in a list of links.
  • Links sit inside sentences that say why the reader would follow them.
  • Every page you care about has at least one inbound link from another page, checked by script rather than by memory.
  • Links to missing pages, generic anchors and deep pages are reviewed each time the site changes.

Questions and answers

How many internal links should a page have?

Google says there is no magical ideal number of links on a page, and that if you think you have too many, you probably do. Add a link where it helps the reader reach something they would plausibly want next.

Should anchor text contain my target keyword?

Only when it reads naturally. Google's guidance says to resist cramming in every related keyword, notes that keyword stuffing violates its spam policies, and suggests asking whether the reader needs those words to understand the next page.

Is a menu link enough, or do I need links inside the text?

Google says every page you care about should have a link from at least one other page, and that the words before and after a link matter. A menu satisfies the first and cannot supply the second, so important pages also deserve links inside relevant text.

Can I use a button or a click handler instead of a link?

For navigation, no. Google generally can only crawl an a element with an href attribute and cannot reliably extract URLs from script events. MDN adds that fake links also break copying, new tabs and assistive technology.

Sources

  1. Link best practices for Google (Search Central) developers.google.com
  2. SEO Starter Guide (Search Central) developers.google.com
  3. Understanding SC 2.4.4: Link Purpose (In Context) (W3C) w3.org
  4. The Anchor element (MDN) developer.mozilla.org
  5. Links do not have descriptive text (Lighthouse) developer.chrome.com