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.

| Check                                     | Severity |
| :---------------------------------------- | :------- |
| A collection with no pages in it          | error    |
| A folder colliding with a URL segment     | error    |
| A missing `title` or `description`        | warning  |
| A broken internal link or anchor          | warning  |
| What a generated section could not read   | warning  |
| What each language carries, and what has stood still | note |

## 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](/concepts/generated-sections) 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.

::callout{type="tip" title="These used to be printed and lost"}
An artefact is parsed while the config is loading, so a warning about a dropped
`$ref` was printed before the dev server had finished starting and had scrolled
away by the time anyone looked — while every other finding this layer produces
waited in one report. They come through the report now, and through the
[Checks](/reference/devtools/checks) panel with 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.

```bash
pnpm exec duxt report            # Markdown
pnpm exec duxt report --json     # the same data, unrendered
pnpm exec duxt report --help     # what it takes, and what it exits with
```

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, and `2` when it could not understand the command line — which is
a statement about your invocation rather than about your documentation.

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.

::callout{type="warning" title="It reports the last parse, not this second"}
The pages come from Content's cache, so a page written since the last build or
dev run is not in it. Run either one first — the report says so plainly when it
finds no cache at all, rather than reporting every collection as empty.
::

## 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.

## Write links as absolute documentation paths

`/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.