Jeder Fehler, den diese Ebene tatsächlich hatte, war lautlos: eine Collection,
die Content verwarf, weil ihr Name kein JavaScript-Bezeichner war; eine Seite,
die 404te, weil ein Link veraltet war; eine Navigation, die leer zurückkam.
Keiner der drei sagte etwas — der Build war grün und die Seite falsch. Also
laufen nach dem Build Prüfungen, die alle dasselbe lesen: den Parse-Cache, den
Content hinterlässt.

| Prüfung                                      | Schweregrad |
| :------------------------------------------- | :---------- |
| Eine Collection ohne Seiten                   | Fehler      |
| Ein Ordner, der mit einem URL-Segment kollidiert | Fehler   |
| Ein fehlender `title` oder `description`      | Warnung     |
| Ein toter interner Link oder Anker            | Warnung     |
| Was eine generierte Sektion nicht lesen konnte | Warnung    |
| Was jede Sprache trägt und was stehen geblieben ist | Hinweis |

## Warum der Schweregrad nicht einheitlich ist

Ein Fehler kostet eine Seite, die niemand erreichen kann und die nichts auf der
Website je erklären würde — eine leere Collection ist ein 404 auf jeder Seite
einer Version. Eine Warnung kostet Qualität: Die Seite rendert weiterhin, und
eine entfernte Quelle kann zwischen zwei Releases veralten, ohne dass das die
Schuld dieses Builds wäre. Den Build scheitern zu lassen, weil das Repository
eines anderen weitergezogen ist, würde einen grünen Build von einem Dritten
abhängig machen.

Ein **Hinweis** kostet gar nichts: Er ist ein Zustand der Website, kein Mangel
an ihr. Eine unübersetzte Seite ist kein Fehler, und sie als Warnung zu melden
würde allen beibringen, über die Warnungen hinwegzulesen.

## Generierte Sektionen

Eine [generierte Sektion](/concepts/generated-sections) wird geprüft wie jede
andere Collection, in ihren eigenen Worten. Was in diesem Bericht landet, ist
eines von drei Dingen:

- Das **Artefakt war an diesem Ref nicht da**, die Sektion wurde also nicht
  gebaut;
- der Typ **hat nichts daraus gelesen**, die Sektion beansprucht also URLs und
  liefert keine aus;
- der Typ ist über **etwas hinweggegangen** — ein nicht auflösbares `$ref` in
  einem OpenAPI-Dokument, eine Changelog-Überschrift, die wie ein Release
  aussieht, sich aber nicht als eines parsen lässt und deshalb Teil des Releases
  darüber wurde.

Alle drei sind Warnungen, und das ist kein Kompromiss. Alles, was ein **lokales**
Artefakt falsch machen kann — ein Pfad, der nicht da ist, eine Datei, die der Typ
überhaupt nicht lesen kann, eine Option, die er nicht kennt —, lässt den Build
bereits beim Laden der Konfiguration scheitern, lange bevor es diesen Bericht
gibt: Die eigene Konfiguration der Website ist ein Fehler, den der Build nicht
mittragen darf. Was hier übrig bleibt, ist entweder ein **entferntes** Artefakt,
das zwischen zwei Releases legitim veraltet sein kann, oder eine Seite, die
gerendert wurde und in der etwas fehlt.

::callout{type="tip" title="Früher wurden sie gedruckt und gingen verloren"}
Ein Artefakt wird beim Laden der Konfiguration geparst, eine Warnung über ein
verworfenes `$ref` wurde also gedruckt, bevor der Dev-Server fertig gestartet
war, und war weggescrollt, wenn jemand hinsah — während jeder andere Befund
dieser Ebene in einem Bericht wartete. Sie kommen jetzt über den Bericht, und mit
ihm über das Panel [Prüfungen](/reference/devtools/checks).
::

## Der Übersetzungsbericht

Eine Zeile je Sprache, in jedem 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.
```

Die zweite Zahl braucht `history: true` an der Quelle — die Daten kommen aus
git, und eine Quelle, die ihre Historie nicht angefordert hat, hat keine. Ohne
sie zählt der Bericht die Abdeckung und sagt nichts über Veralten, statt aus
dem Fehlen zu raten.

Es gibt ihn wegen eines konkreten Fehlschlags. OpenCode übersetzte seine
Dokumentation mit einem Agenten in CI in siebzehn Sprachen, schaltete den
Workflow ab, und nirgends stand, dass die Übersetzungen sich nicht mehr bewegten.
Die Ebene weiß pro Seite ohnehin, welche Sprachen sie tragen und wann jede Datei
sich zuletzt geändert hat; ein Bericht macht daraus etwas, das jemand sieht.

## Derselbe Bericht als Befehl

Die Befunde liegen an zwei Orten, die sich schlecht jemandem in die Hand drücken
lassen: in einem Build-Log, das weggescrollt ist, und in Devtools-Panels, die es
nur in einem laufenden Dev-Server gibt. `duxt report` ist der dritte — dieselben
Daten als Markdown auf stdout. Er kommt mit dem Paket, die Ebene zu erweitern
ist also alles, was es dafür braucht.

```bash
pnpm exec duxt report            # Markdown
pnpm exec duxt report --json     # the same data, unrendered
pnpm exec duxt report --help     # was er annimmt und womit er endet
```

Er liest die `app.config.ts` der Website, die Artefakte daneben und die Seiten
aus dem Parse-Cache von Content — **kein Server**, er läuft also in CI und in
einer Pipe. Er endet mit `1` bei einem Fehler und mit `0` bei Warnungen, also
nach derselben Schweregradregel, der auch der Build folgt, und mit `2`, wenn er
die Kommandozeile nicht verstanden hat — eine Aussage über deinen Aufruf, nicht
über deine Dokumentation.

Drei Panels haben hier keine Entsprechung, und das mit Absicht: Der Suchindex,
der Pfad-Debugger und die Cache-Auflistung sind Fragen an einen laufenden
Prozess, und sie von außen zu beantworten hieße raten.

::callout{type="warning" title="Er meldet den letzten Parse-Lauf, nicht diese Sekunde"}
Die Seiten kommen aus dem Cache von Content, eine Seite, die seit dem letzten
Build- oder Dev-Lauf geschrieben wurde, steht also nicht darin. Lass zuerst einen
von beiden laufen — der Bericht sagt das deutlich, wenn er gar keinen Cache
findet, statt jede Collection als leer zu melden.
::

## Was die Prüfungen wissen und du nicht

- **Eine Ordnerkollision** ist nur ein Problem, solange ein Präfix aktiv ist. Ein
  Doku-Ordner namens `v2` ist auf einer unversionierten Seite in Ordnung und auf
  einer versionierten unerreichbar, weil das Präfix gewinnt. Der Build weiß,
  welcher Fall vorliegt, und die Meldung benennt die Datei und bietet die Lösung
  an.
- **Ein Link wird so aufgelöst, wie das Theme ihn auflöst.** Ein absoluter Pfad
  in einer Seite ist relativ zur eigenen Quelle dieser Seite, wird also zuerst
  unter dem Präfix der Quelle probiert und danach nackt. Eine Prüfung, die nur
  die nackte Form kennt, würde auf einer Multi-Repo-Seite jeden korrekten Link
  melden.
- **Ein Link wird in seiner eigenen Sprache aufgelöst.** Jede Sprache einer
  Quelle liefert identische Inhaltspfade — die Locale steht vor der URL, nicht
  im Inhaltsbaum —, und Anker werden aus dem Überschriftentext abgeleitet, eine
  deutsche Überschrift hat also einen deutschen Anker. Eine Prüfung, die nur
  über den Pfad auflöst, würde einen deutschen Link gegen englische
  Überschriften prüfen und auf einer übersetzten Seite jeden korrekten Anker
  melden. Ein Link auf eine Seite, die eine Sprache nicht trägt, fällt auf das
  Original zurück — genau wie die Website selbst.
- **Externe URLs werden nicht geprüft.** Eine zu verifizieren hieße, das Netz in
  den Build zu holen, und ein nicht erreichbarer fremder Host ist kein Grund,
  warum deine Dokumentation nicht ausgeliefert wird.

## Schreibe Links als absolute Dokumentationspfade

`/concepts/sources`, nicht `../2.concepts/2.sources.md`. Unter welchem Präfix
eine Seite ausgeliefert wird, entscheidet sich zur Build-Zeit — Pfade ohne
Präfix zu schreiben ist deshalb genau das, was dasselbe Markdown auf einer Seite
`/guides/deploying` und auf einer anderen `/acme/v2.0.0/guides/deploying`
bedienen lässt. Es ist außerdem die einzige Form, die die Linkprüfung sieht.