Internal links: help visitors and crawlers find pages

How internal links work for visitors and search engines: link text, link counts, orphan pages, and a script that shows click depth and broken links.

BeginnerHow-to6 min readUpdated October 30, 2026

In this guide
  1. What internal links do
  2. How many links a page has
  3. Write link text that works
  4. Use real links
  5. Give every page a path
  6. Shape the structure
  7. Links to other sites
  8. The speed side
  9. How to check
  10. Mistakes to avoid
  11. Check your own page
  12. Sources

Lighthouse explains the crawler side. Search engines may use href attributes on links to crawl a site, and the href should point to an appropriate destination so that more pages can be discovered. A page that nothing links to can be missed, even if it is in your content system.

Link text does a second job. Lighthouse's documentation says that when link text clearly conveys the target, both users and search engines can more easily understand your content and how it relates to other pages. It adds: pay attention to your internal links too. Improving their quality can help both users and search engines navigate your site.

MDN makes the same point from the accessibility side. Assistive software can list all the links on a page, and strong link text benefits all users, because that list works like the way sighted visitors scan a page.

The Almanac's view is balanced. Links are still a crucial signal, it says, but the days of more links simply meaning higher rankings are largely over. Link signals now sit beside content quality, relevance and user experience.

In the 2025 Web Almanac, the median page had 43 links to its own site on desktop and 39 on mobile. At the 90th percentile the figures were 174 and 161. The Almanac links the high end to large sites with mega-menus and dense footers. External links were far fewer: a median of six, and 25 or 24 at the 90th percentile.

Menus and footers repeat on every page, so they make up much of the count. A long menu is also part of the page's markup. See DOM size. A short, clear menu that repeats everywhere, plus links inside the text to related pages, serves visitors and crawlers better than a menu that lists everything.

Lighthouse flags these generic phrases: "click here", "click this", "go", "here", "this", "start", "right here", "more" and "learn more". Its advice is to replace them with specific descriptions that tell the reader what they will get.

Weak link textStrong link text
"click here", in "To see our guides on images, click here.""guides on images", in "Read our guides on images."
"learn more""how responsive images work"
"https://example.com/pricing/""how we scope a project"

More of Lighthouse's rules:

In the Almanac, the share of pages passing Lighthouse's link text check in 2025 was 84.64% of home pages on desktop and 86.64% on mobile, and 92.42% and 93.45% for inner pages. Home pages score lower, the Almanac says, probably because of generic calls to action. An image that is a link needs alt text that names the action. See image SEO and speed.

A link needs an a element with an href. A span or button that changes the page with script gives crawlers no address to follow. Lighthouse 13.5 flags javascript:void(0) links and anchors with no href that rely on a click handler. See JavaScript and SEO.

Give every page a path

A page with no links pointing to it is an orphan. The other measure is click depth: how many clicks it takes to reach a page from the home page. Both are easy to measure by following links, as a crawler would. This script does that on one site. It needs only Node 18 or later:

// crawl.mjs: node crawl.mjs https://example.com/ [maxPages]
const start = new URL(process.argv[2]);
const max = Number(process.argv[3] || 300);
const pages = new Map([[start.href, { depth: 0, inbound: 0, status: 0, from: new Set() }]]);
const queue = [start.href];

while (queue.length && pages.size <= max) {
  const url = queue.shift();
  const page = pages.get(url);
  const res = await fetch(url, { redirect: 'manual' }).catch(() => null);
  page.status = res ? res.status : 0;
  if (!res || res.status !== 200 || !(res.headers.get('content-type') || '').includes('text/html')) continue;
  const html = (await res.text()).replace(/<(script|style)[\s\S]*?<\/\1>/gi, ' ');
  for (const m of html.matchAll(/<a\s[^>]*href=["']([^"']+)["']/gi)) {
    let link;
    try { link = new URL(m[1], url); } catch { continue; }
    if (link.origin !== start.origin) continue;
    link.hash = '';
    const known = pages.get(link.href);
    if (known) { known.inbound++; known.from.add(url); }
    else { pages.set(link.href, { depth: page.depth + 1, inbound: 1, status: 0, from: new Set([url]) }); queue.push(link.href); }
  }
}

const path = (u) => u.replace(start.origin, '') || '/';
const all = [...pages];
console.log(all.length, 'addresses found; deepest page is', Math.max(...all.map(([, p]) => p.depth)), 'clicks from the start');
for (const [u, p] of all.filter(([, p]) => p.status >= 300)) {
  console.log(p.status >= 400 || p.status === 0 ? 'BROKEN  ' : 'REDIRECT', path(u), '<- linked from', [...p.from].slice(0, 3).map(path).join(', '));
}
console.log('fewest links in:', all.filter(([, p]) => p.status === 200).sort((a, b) => a[1].inbound - b[1].inbound).slice(0, 3).map(([u, p]) => `${path(u)} (${p.inbound})`).join(', '));

I tested it on a small made-up site with a redirect, a missing page, a chain of pages three clicks deep and one page that nothing links to. It found seven addresses, named the redirect and the broken link with the page that links to each, and reported the depth of three. It did not find the orphan, which is how you know it is one: compare the pages it found with the full list of pages from your content system or your sitemap. Any page on the list that the crawl did not reach needs a link. I also ran it on a local build of 120 pages. It found them all, the deepest was two clicks from the start, and there were no redirects or broken links.

The script is simple. It reads only the HTML that the server sends, and it does not run scripts. If your menus are built by script, it will not see them, and neither might a crawler. See JavaScript and SEO.

Shape the structure

The Almanac found noopener on 40.9% of pages and noreferrer on 25%. Neither affects ranking, it says: they protect visitors. MDN notes that target="_blank" already provides the noopener behavior. For search, nofollow was on 32.3% of pages, while the finer sponsored and ugc values were on only 0.5% each. MDN describes nofollow as telling search engines to ignore the link relationship, and says it may mean the page's owner does not endorse the target.

The speed side

How to check

  1. Run the crawl script on your home page, and compare its list with your full list of pages.
  2. Read the report for redirects and broken links, and change each link to the final address.
  3. Run Lighthouse and read Links have descriptive text and Links are crawlable.
  4. Read your main menu and footer as a list. Would a stranger know where each one goes?
  5. Open your most important page and ask where a reader goes next. If there is no link, add one.

Mistakes to avoid

Check your own page

Run the free audit and read the SEO findings on links. Then run the crawl script on your home page, and write down the three pages with the fewest links pointing to them.

Sources

Start here

Free website audit

Live Google data for any public page: mobile and desktop.

Mobile + desktop · Lighthouse lab test · Real-user Chrome data when Google has it

Rather have this done for you? Technical SEO · Book a call

Related guides

All guides →