Skip to content
duxt

What the build checks

The silent failures this layer had, turned into messages.

Every bug this layer actually had was quiet: a collection Content dropped because its name was not a JavaScript identifier, a page that 404ed because a link had gone stale, a navigation that came back empty. None of the three said anything — the build was green and the site was wrong. So the checks run after the build, all reading the same thing: the parse cache Content leaves behind.

CheckSeverity
A collection with no pages in iterror
A folder colliding with a URL segmenterror
A missing title or descriptionwarning
A broken internal link or anchorwarning
What a generated section could not readwarning
What each language carries, and what has stood stillnote

Why severity is not uniform

An error costs a page that nobody can reach and nothing on the site would ever explain — an empty collection is a 404 on every page of one version. A warning costs quality: the page still renders, and a remote source can go stale between one release and the next without that being this build's fault. Failing the build on somebody else's repository having moved on would make a green build depend on a third party.

A note costs nothing at all: it is a state of the site rather than a defect in it. An untranslated page is not a mistake, and reporting it as a warning would train everyone to read past the warnings.

Generated sections

A generated section is checked like any other collection, in its own words. What reaches this report is one of three things:

  • the artefact was not there at that ref, so the section was not built;
  • the type read nothing out of it, so the section claims URLs and serves none;
  • the type carried on past something — an unresolvable $ref in an OpenAPI document, a changelog heading that reads like a release but does not parse as one and therefore became part of the release above it.

All three are warnings, and that is not a compromise. Everything a local artefact can get wrong — a path that is not there, a file the type cannot read at all, an option it does not know — already fails the build while the config is loading, long before this report exists: the site's own configuration is a mistake the build must not carry. What is left here is either a remote artefact, which may legitimately have gone stale between releases, or a page that rendered with something missing from it.

The translation report

One line per language, in every build:

[duxt] translations
[duxt]   de: 118/121 pages, 3 behind the original
[duxt]     "docs/guides/deploying.md" has no de translation.
[duxt]     "docs/de/concepts/sources.md" has not moved since the original changed.

The second figure needs history: true on the source — the dates come from git, and a source that has not asked for its history has none. Without it the report counts coverage and says nothing about staleness, rather than guessing from the absence.

It exists because of a specific failure. OpenCode translated its documentation into seventeen languages with an agent in CI, switched the workflow off, and nothing anywhere said the translations had stopped moving. The layer already knows, per page, which languages carry it and when each file last changed; a report is what turns that into something anybody sees.

The same report, as a command

The findings live in two places that are hard to hand to anybody: a build log that has scrolled away, and devtools panels that exist only inside a running dev server. duxt-report is the third — the same data as Markdown on stdout. It ships with the package, so extending the layer is all it takes to have it.

pnpm exec duxt-report            # Markdown
pnpm exec duxt-report --json     # the same data, unrendered

It reads the site's app.config.ts, the artefacts beside it and the pages out of Content's parse cache — no server, so it runs in CI and in a pipe. It exits 1 on an error and 0 on warnings, which is the same severity rule the build follows.

Three panels have no equivalent here, and deliberately: the search index, the path debugger and the cache listing are questions about a running process, and answering them from the outside would be guessing.

What the checks know that you do not

  • A folder collision is only a problem while a prefix is active. A docs folder called v2 is fine on an unversioned site and unreachable on a versioned one, because the prefix wins. The build knows which, and the message names the file and offers the fix.
  • A link is resolved the way the theme resolves it. An absolute path written in a page is relative to that page's own source, so it is tried under the source's prefix first and bare second. A checker doing only the bare form would report every correct link on a multi-repo site.
  • A link is resolved in its own language. Every language of one source serves identical content paths — the locale sits in front of the URL, not in the content tree — and anchors are derived from heading text, so a German heading has a German anchor. A checker resolving by path alone would check a German link against English headings and report every correct anchor on a translated site. A link to a page a language does not carry falls back to the original, which is what the site does too.
  • External URLs are not checked. Verifying one would put the network in the build, and an unreachable third-party host is not a reason your documentation fails to ship.

/concepts/sources, not ../2.concepts/2.sources.md. The prefix a page is served under is decided at build time, so writing paths without one is what lets the same Markdown serve /guides/deploying on one site and /acme/v2.0.0/guides/deploying on another. It is also the only form the link checker sees.

Was this page helpful?