[linkcheck]

External link checking configuration ([linkcheck]).

Governs ubc build linkcheck, which requests every external URL a project references and grades the answer.

Guide: Link checking.

linkcheck.allow_private
Type:
boolean
Default:
false

When true, also check URLs pointing at addresses that are not globally routable. Unset means false.

By default such URLs are reported as unchecked and never requested, because a checker follows URLs written by whoever wrote the document and those ranges include internal services and cloud metadata endpoints. Turn this on for documentation that legitimately links to an intranet.

The refused set is the private ranges, loopback, link-local (which covers 169.254.169.254, the cloud metadata endpoint on every major provider), CGNAT, the unspecified and broadcast addresses, and the RFC 5737 documentation ranges — plus, for IPv6, loopback, the unspecified address, and the IPv4-mapped and IPv4-compatible forms of all of those. The host names localhost and anything under .local are refused by name. The check is applied to the URL as written and again to every redirect hop before it is requested; no DNS lookup is performed, so a host name that resolves into a private range is not caught by the address rules.

linkcheck.allowed_redirects
Type:
table of strings
Default:
{}

Expected redirects, as a map of from-pattern = to-pattern.

Both sides are regular expressions searched anywhere in the URL; a match on both makes the result working. Unset or empty reports every non-cosmetic redirect.

Cosmetic ones — a bare host’s slash, a default port, a fragment, a www., a site root to a page on that site — need no entry.

linkcheck.anchors
Type:
boolean
Default:
true

When true, also verify each URL’s #fragment against the anchors the page declares. Unset means true.

A fragment is checked by reading the final page’s HTML and looking for an element whose id matches — or, on an <a>, whose name matches. Matching is exact and case-sensitive, and the percent-decoded spelling is tried as well, so #a%20b finds id="a b".

A page linked with at least one fragment is fetched with a single GET that reads its body, instead of the usual HEAD; every fragment of that page is answered from the one response. A page linked without any fragment is unaffected.

A fragment the page was ASKED about and could not answer is reported as unverified and COUNTED rather than failed: a page that is not HTML, a body larger than the size limit, a response in an encoding that was not requested, a page whose bytes arrived in an encoding that could not be decoded, a page that did not answer 2xx, and the client-side fragments the built-in quirks recognise. A fragment on a URL that was never requested at all — one an ignore pattern covers, an unsupported scheme, or one served from a stored result under offline that had never been asked that anchor — is counted separately as not_checked, because those URLs already have their own verdict and counting their anchors as unverified would make that number impossible to gate on. Both appear on the summary line and in --output-format json. Only a fragment absent from a page that was read in full becomes a linkcheck.anchor_missing finding.

Turn this off to check URLs alone.

linkcheck.cache_days
Type:
integer
Default:
5

How long a check result stays usable, in days:

  • > 0 — a stored result expires after this many days;

  • 0 — always re-check (the freshness test is never satisfied);

  • negative — never expire.

Unset means 5. Only successes and redirects are ever stored; a failure is never cached, so a link that has been fixed is noticed on the next run. The one exception to THAT rule is a MISSING ANCHOR: it is stored beside the page’s own result and expires with it, so a linkcheck.anchor_missing can be re-reported for up to this many days after the anchor is added. --refresh, or ubc build linkcheck cache clean <url>, re-asks sooner.

Freshness is applied when the cache is READ, so changing this value does not discard anything. The one exception to the FRESHNESS rule is offline, which serves expired results too and reports how many.

linkcheck.deny
Type:
string

The severity at or above which a finding fails ubc build linkcheck.

Unset means error, so by default only a verified-dead link (404, 410, a refused connection, a failed DNS lookup or a TLS failure) fails the command; redirects, bot-blocks, server outages and timeouts are reported but do not. Set warning to fail on those too, or none to report without ever failing. The --deny flag overrides this key.

Values:

  • "none": Never fail on findings.

  • "info": Fail on any finding, informational ones included.

  • "warning": Fail on any warning-or-worse finding.

  • "error": Fail only on errors.

linkcheck.hosts
Type:
table of tables
Default:
{}

Per-host politeness limits, keyed by host name ("docs.example.com").

The key is matched against the host of each URL, case-insensitively (ASCII), and without the scheme, the port or any path. A host with no entry uses the built-in limits: at most 2 requests in flight and at least 250ms between two request starts. Unset or empty means every host uses those.

An internationalised host must be written in its punycode form (xn--bcher-kva.example), which is the form a URL is requested under; a Unicode spelling never matches.

Use this for a host that is slow, rate-limited, or that a project links to hundreds of times.

Each entry: Politeness limits for one host ([linkcheck.hosts."example.com"]).

linkcheck.hosts.<name>.max_concurrent
Type:
integer

How many requests to this host may be in flight at once. Unset means the built-in 2, and 0 is treated as 1.

linkcheck.hosts.<name>.min_interval_ms
Type:
integer

The shortest gap, in milliseconds, between two requests to this host. Unset means the built-in 250. A larger value is politer and slower; 0 removes the gap entirely and should be used only for a host you control.

linkcheck.ignore
Type:
array of strings
Default:
[]

Regular expressions matched against each full URL; a URL matching any of them is reported as ignored and never requested.

Patterns are searched ANYWHERE in the URL, so example\.com covers every URL on that host; anchor with ^ or $ to require a prefix or a whole match. A redirect INTO a matching URL is suppressed too. Unset or empty means nothing is ignored.

To suppress findings for particular source files instead, use [lint.per-file-ignores] with the linkcheck.* codes.

linkcheck.max_redirects
Type:
integer
Default:
10

How many redirects one URL may go through before it is reported as broken. Unset means 10.

linkcheck.offline
Type:
boolean
Default:
false

When true, never perform network requests: serve stored results, and report a URL with no stored result as unchecked. Useful for a network-restricted CI job with a pre-warmed cache. Unset means false.

Ignores cache_days and serves EXPIRED results as well as fresh ones — a stale answer beats no answer when there is no way to get a fresh one. The run reports how many it did that for, on the summary line and as summary.stale in the JSON output, so a job that must not run on expired data can gate on that number.

The --offline flag turns this on for one run. When this key is true, --refresh is refused rather than silently ignored.

linkcheck.rate_limit_ceiling
Type:
float
Default:
300.0

The longest wait, in seconds, that a rate-limited host can ask for and still be waited out. Unset means 300.0.

A server answering 429 Too Many Requests with a longer Retry-After than this — and a server that keeps deferring until the waits add up to more than this — makes the URL report as blocked rather than holding the run open. A value that is zero, negative or not a number falls back to the default rather than to “wait indefinitely”.

linkcheck.request_headers
Type:
table of (table of strings)
Default:
{}

Extra request headers, keyed by URL prefix ("https://api.example.com/").

The key must be an absolute http/https URL prefix, and its HOST is what the headers are scoped to: a URL gets them only when its host is exactly that host AND the URL starts with the prefix. A bare host name, a key with userinfo before the host, a key with a #fragment and an empty key are all configuration errors naming the key.

The headers of the LONGEST matching prefix are sent, and only those — the entries do not accumulate — so a general prefix and a specific one can disagree without ambiguity. A prefix is compared against the URL in the one spelling the checker uses (lowercase scheme and host, a bare host given its / path), so write the prefix as you would the URL. Unset or empty means only the standard headers are sent.

A header VALUE of the form ${VARIABLE} is replaced with that environment variable at the start of the run, so a token stays out of the file. Header names are never expanded. A variable that is not set, a ${ that is never closed and an empty ${} are each a configuration error naming the key, never a request sent with the literal text. A header name or value HTTP does not admit is refused there too.

Headers are NOT forwarded across a redirect to a different host: the host is matched against each URL as it is requested, so a credential written for one host is never sent to another.

linkcheck.timeout
Type:
float
Default:
30.0

Timeout, in seconds, for each HTTP request. Unset means 30.0. A value that is zero, negative or not a number falls back to that default and is reported.

linkcheck.user_agent
Type:
string
Default:
""

The User-Agent sent with each request. Unset or empty means ubc/<version>, which identifies the tool honestly.

Some sites refuse requests from any agent they do not recognise. Setting a browser-like string here works around that; it is offered as a choice rather than done by default.

linkcheck.workers
Type:
integer
Default:
8

How many URLs are checked in parallel. Unset means 8, and 0 is treated as 1.

Per-host limits apply independently of this: at most 2 requests to any one host at a time, and at least 250ms between two requests to it. A higher worker count therefore speeds up a project that references many hosts, not one that references a few.