What it refuses to build
Every check the library performs fails the build, instead of shipping a defect that is invisible in a browser and expensive weeks later in search.
Everything below is a defect that ships silently and costs weeks to find. The library refuses it at build time instead, which is the only moment it is cheap.
Routes
| Refused | Because |
|---|---|
| A path no route declares | the page would render a canonical and be absent from the sitemap |
| Two routes on one path | the head and the sitemap would describe different routes under one URL |
| A locale a route does not serve | the canonical would name a page that was never built |
| A collection entry in a locale the site omits | the content and the declaration contradict each other, and neither wins |
A path that does not start with /, or ends with one | both double into every canonical and every alternate on the site |
path: "" on a route with a fixed locale | the empty path is the home page under /[locale]; outside that segment it is /, a path like any other |
Site
| Refused | Because |
|---|---|
baseUrl with a path, or a relative one | it prefixes every absolute URL the site emits |
baseUrl that is not http or https | same, and the scheme reaches the sitemap |
| A site serving no locale | there is no cluster to build and no x-default to point anywhere |
| Duplicate locales | the alternates would list the same page twice |
| A malformed language tag | locale.invalid, which exists because these tables get copied wrong |
| A locale ICU has no likely region for | og:locale is language_TERRITORY and there is nothing to derive from |
A sitemap priority outside 0.0 to 1.0 | the value is silently dropped by crawlers rather than corrected |
A locale with no territory is not refused: pt becomes pt_BR and en becomes
en_US from ICU's likely subtags, which is the same table both sides used to
hand-copy. Only a tag ICU gives no likely region for needs localeTags.
Two things it gets right that hand-written versions usually do not
Your URL keeps the case you wrote, and the tag takes its canonical form.
Declare pt-br and you get /pt-br/ in the canonical, hreflang="pt-BR" in
the cluster, lang="pt-BR" on the document and pt_BR in og:locale. BCP 47
makes case insignificant, so those are one tag and not two, and emitting the
canonical form is what keeps lang and hreflang from answering the same
question two different ways in one document.
This is worth spelling out because the library did the opposite for a while. It
used to echo the string you declared, to work around a crawler that compared the
URL segment against the tag byte for byte and reported a translation hole for a
language already served. That was the wrong layer to fix it in: goflag now folds
the two to one identity, as BCP 47 says they are, and the library went back to
emitting the canonical form in 0.2.0.
x-default is in its own cluster. A x-default that points outside the set
of alternates is a cluster that does not close, and reciprocity checks fail on
it.
The tests are the rules
The library's own suite does not invent its assertions. Each one is named after
the goflag rule it covers: hreflang reciprocity, x-default inside its own
cluster, sitemap and <head> in agreement, og:locale territorialised.
That file is explicitly provisional. When goflag rules --json ships the
catalogue as data, it is replaced by a harness that renders the library's output
and evaluates the real rules against it. A rule added to the catalogue then
breaks the library if the library does not satisfy it, which is the loop worth
having rather than an accident to correct.
What it deliberately does not do
- It will not write
og:image:altfor you. A library cannot describe a card it did not draw, and the only value always available — the page title — is the one the protocol excludes: "a description of what is in the image (not a caption)". PassimageAltbesideimage, or the tag is omitted and an auditor tells you so. Synthesising it would satisfy the check that looks for it, which is worse than failing: the defect would never be reported again. - No
next-intlwrapper. Forty lines with nothing added. - No React components. They drag a design system in with them.
- No
process.env. See the registry. - No JSON-LD yet. The catalogue does not judge structured data beyond its presence, and producing what nobody can check is how signals get collected and never read.
- No Pages Router. App Router only.