Link & image checker · Playbook
Link thresholds — the numbers Link & image checker grades on, and where they come from
Issue #70: these were hardcoded in klink.mjs and surfaced nowhere, so “why is that a warning?”
had no checkable answer. This is the reader-facing side of the source field each finding carries.
Read the caveat before quoting a number. Only the broken-link rule is absolute — a 404 is a 404. Everything below it is a default tuned for a general site, not a standard. A documentation site with 4,000 legitimate internal links, a directory page whose job is to be a list of links, or a design system where “Learn more” is the deliberate house pattern all have a different right answer. The right band depends on the site.
What fails the page
| Check | Threshold | Level |
|---|---|---|
| Broken link | final status ≥ 400, or a network error / timeout | error |
| Broken image | as above | error |
| Parked domain | registrar parking markers in the response | error |
A CDN/WAF bot-challenge (Cloudflare, WP Engine GES) is not counted broken — a real browser passes
it. Those are reported separately as challenged, which is why a protected site does not FAIL here.
Cloudflare /cdn-cgi/ obfuscation hrefs are skipped outright: a browser never fetches them literally.
What flags for review
| Check | Threshold | Level | Why this number |
|---|---|---|---|
| Generic anchor text | ≥ 10% of links with text | warn |
A tenth of a page’s links saying “click here” is a pattern, not an accident. Below that it is usually one stray CTA |
| Excessive links | > 3,000 on one page | warn |
Google’s long-standing crawl-budget guidance; well above any editorial page |
| High link count | > 1,500 | info |
Advisory only — may dilute link equity |
| Image-only link, no alt | any | warn |
A screen reader announces nothing; there is no threshold to tune |
| Malformed link | any | warn |
javascript:, empty href, bad mailto:/tel:, unencoded spaces |
| Insecure external link | any http:// external |
warn |
Increasingly blocked or auto-upgraded |
target="_blank" without rel="noopener" |
any | warn |
The linked page gets a handle on window.opener |
| Nofollow on an internal link | any | warn |
Almost always unintentional; wastes internal link equity |
| Mixed content | any insecure subresource on an https page | warn |
Active (script/iframe) is graded above passive (image/media) |
The generic-anchor list
^click here$ · ^here$ · ^read more$ · ^learn more$ · ^more$ · ^link$ · ^this$ · ^go$ ·
^see more$ · ^view more$ · ^details$ · ^info$ · ^more info$ · ^more details$ ·
^continue$ · ^continue reading$
Anchored to the whole trimmed anchor text, deliberately: “Read more about dental implants” is a perfectly good anchor and is not matched. Only a bare “Read more” is.
Run limits (not verdicts)
| Setting | Default | Flag |
|---|---|---|
| Links/images fetched per page | 250 each | --max |
| Concurrent fetches | 6 | --concurrency |
| Per-request timeout | 10s | — |
The per-host politeness limiter (safe-fetch.mjs) spaces requests to one host regardless of
concurrency. These defaults exist because an orchestrated run once read a throttled staging host’s
back-pressure as 32 broken links; standalone the same page reported 3.
When the cap bites, klink says so — “checked first 250 of N links (raise with --max)”. A silent
cap would read as “those were all of them”.