Anchor text that does not match the page it links to
A link that says how I work and points at a page about engagements tells a crawler nothing. squirrel compares inbound anchor text with the target's title, H1 and URL.
What an anchor mismatch is
links/anchor-text reads a page’s outgoing links and asks whether the text is empty or generic. This rule asks the question that matters for topical relevance: does the text pointing at a page share anything with that page?
A consultancy site linked how I work to its engagement page twelve times, and it was the only anchor text that page ever received. A blog index received View all posts eighteen times. Neither link is broken or generic in the sense of the other rule, but neither tells a crawler or a reader what the destination is about.
What squirrel checks
The rule is site-wide and emits a single check named anchor-mismatch. It judges each pair of a source page and a target page, never each link: twelve links with the same text on one page are one observation, and the pair’s anchor texts are combined before they are compared.
- Only contextual links count. Links inside a
nav,header,footerorasidelandmark are template text and are left out entirely. - The combined anchor text is compared with the target’s title, its H1 and the words in its URL path. The pair warns only when no meaningful word is shared.
- Words match when they are equal, or when one is a prefix of the other (three or more shared characters, at most three left over), so
Fridgesmatches/repairs/fridge. Two-character words are real words and match by equality, soLGmatches/brands/lg. - Anchor text keeps a space between block-level child nodes, so a card that wraps a heading and a description reads as separate words. Block-level means headings, paragraphs, list items, table cells, buttons, labels and
svg. Inline elements add no gap:Re<b>start</b>is one word. Text insidescript,style,templateandnoscriptis not anchor text. Boundaries come from the tag name and an inlinestyle="display:block"(orflex,grid,table,list-item); CSS from a stylesheet or a class cannot be seen at parse time. A link whose text joins adjacent elements with no whitespace and no boundary (a card whose blocks come from a class) is marked as glued, and the words may really be separate. For a glued link, a pair counts as sharing a word when a target word of three or more characters sits inside an anchor word, or when one of the card’s own pieces shares a word with the target, which is how two-character brands such asLGmatch. Not every glued card passes: with no shared word the pair still warns. The stored pieces are cut at 200 characters and 16 pieces per link; a link whose pieces were cut has lost evidence, so with no shared word it is counted as not judged. - Letters are compared without accents, so
CafématchesCafe menu. - Only Latin, Cyrillic and Greek words are compared. Text in any other script (Chinese, Japanese, Thai, Arabic, Hebrew, Korean, Devanagari and the rest) has no word boundaries this rule can compare. A pair whose anchor text or target contains a letter from one cannot be judged by shared words and is counted in
details.notJudged. The whole anchor text is compared however long, up to 64 distinct words per pair; a longer text with no shared word in its first 64 is counted as not judged, never warned on. - The target’s full title and every H1 are compared, however long; a word longer than 64 characters, or a word list cut off by the per-page cap of 64 distinct words, leaves the pair not judged unless a word was already shared.
?p=123is a real page address (WordPress’s default permalink), not pagination; onlypage,pagedandpagenumparameters and/page/Npaths are. - Pages are matched by the exact URL the crawler stored, query string included: a tracking-parameter variant such as
?utm_source=newsof a link counts only when the crawler stored that exact URL. - Empty anchor text and anchor text made only of stop words does not count as a distinct anchor text, so it cannot hide the escalation clause.
- A target with no title or H1 whose URL has no alphabetic word (
/p/123) is not judged. Pages that differ only by query string (?id=1,?id=2) are separate pages. - Only pages that answered 2xx with HTML are targets. A 404 titled
404 Not Found, a redirect and a PDF are counted indetails.notJudged. Fragment links (/docs/api#authentication), links markedrel="next"orrel="prev",nofollowlinks and links to paginated URLs (/page/2,?page=3) are left out. - If the target’s title or H1 is in a script written without spaces, or gives no word at all, only its URL words would remain to compare against, so the pair is not judged. The same holds when the anchor text mixes in such a script.
- Letters that do not decompose (
ø,ł,ß,æ,đ,þ) are folded too,l,ss,ae,d,th. - Privacy, terms, contact, legal, cookie, accessibility, sitemap, sign-in, cart and search pages are never targets, and neither is the homepage or the page the audit started from. A path of
/with a query string, such as WordPress’s/?p=123or/?page_id=5, is a page of its own and is judged, unless it is the page the audit started from. - A pair that cannot be judged (empty anchor text, only stop words such as
the, or a target with no title, H1 or URL words) is counted indetails.notJudged, not dropped silently.
Outcomes:
- Skipped when there are too few pages. Message:
Insufficient pages for anchor text analysis. - Warn when at least one pair shares no word. Message:
2 link pair(s) to 1 page(s) use anchor text that shares no word with the target; for 1 of them it is the only contextual anchor text the page receives. Each item gives the source and target, the anchor text, the target URL and the target’s title. The escalation clause appears when the mismatching text is the only distinct contextual anchor text a page receives. It is left out for a crawl that stopped at its page cap, because the claim rests on links not seen. Severity warning, weight 3. - Skipped when nothing could be compared. Message:
No link pair had anchor text and a target to compare. A pass needs at least one judged pair. - Pass otherwise. Message:
Anchor text shares a word with the page it points at.
details carries judged, notJudged, total (all mismatching pairs; the item list is capped at 100) and droppedPairs (distinct link pairs to crawled pages that were not recorded on a very large crawl; any dropped pair also turns the escalation clause off).
How to fix it
Rewrite the anchor so it names the destination, using the words the target’s title or heading uses: how I work becomes how we engage with clients, and View all posts becomes Read the blog. If a page’s title does not describe the page, fix the title instead.
| Rule ID | links/anchor-mismatch |
| Category | Links |
| Scope | Site-wide |
| Severity | warning |
| Weight | 3/10 |
Options
This rule has no options.
Enable / disable
Disable this rule
[rules]
disable = ["links/anchor-mismatch"]Disable all Links rules
[rules]
disable = ["links/*"]Enable only this rule
[rules]
enable = ["links/anchor-mismatch"]enable replaces the default ["*"], so this is the only rule selected. Leave disable out: a rule that matches any disable pattern is skipped even when enable names it. Only an enabled setting under a rule’s own [rule_options] table overrides both lists.
Related rules
- links/anchor-text: empty and generic anchor text on a page’s outgoing links.
- links/no-contextual-inbound: pages linked only from sitewide chrome.
- links/internal-links: how many internal links a page carries.
Links findings ship in every audit next to the SEO, performance and agent experience rules. See Fix SEO issues with AI for how an agent works through a report.
Glossary: Anchor text, Internal linking.
References
Check your site
Run squirrel audit https://example.com and open the Links section of the report. Each mismatching pair is listed with its anchor text and target. Signed-out local audits need no account and cost nothing. Signed-in audits spend credits.