Content
Checks
Structural checks that fail the build instead of the visitor. The link audit resolves every in-prose link against the site's address space, and reports a broken one as a named error at prerender and when you open the page in dev.
A broken internal link is a defect you would rather find yourself at build time than have a visitor find at runtime. Site checks run over the whole corpus as data, report what they find as named findings, and can fail the build. The link audit is built in, and you can add checks of your own.
Any site with cross-references, which means any docs site, should turn on links(). It is what lets you rename and reorganize pages without worrying, because when you move a page, every stale link to it fails the build with a clear error.
The link audit
Pass checks to site(), and links() resolves every markdown link in every body against the site’s address space:
export const docs = site({ outline: docs, checks: [links()] });A link to a page that does not exist, or to an #anchor that no heading produces, is a finding. This site runs the audit, so every cross-link in this section was verified by it; while one of these pages linked to a sibling that had not been written yet, the build refused to pass until the sibling existed.
It reports in two places:
- At prerender, a broken link fails the build with the page, the link text and the target.
- In dev, the same finding appears when you open the page, so you catch it while you write.
The audit knows the whole address space, including dimension coordinates and declared redirects, so it can tell a genuinely dead link from one that resolves through an alias.
Findings as data
docs.check() runs every check over the corpus and returns the findings as plain data without ever throwing, so you can assert on them in a test or a CI step independently of a prerender crawl:
// a vitest guard: the docs have no broken links
import { docs } from '$lib/site.server';
test('no broken links', async () => {
const findings = await docs.check({ base: '/docs' });
expect(findings).toEqual([]);
});This matters most on a dynamic site, where the prerender crawler never runs and this check is your only structural safeguard.
Custom checks
A check is a named object with a page hook, a site hook or both, each returning findings. page runs per slug inside load, so dev reports it when you open the page and an error-severity finding throws. site runs once over the whole corpus, through docs.check():
// a house rule: every page fills its summary
import type { Check, Finding } from '@ogygia/content';
export function requireSummary(): Check {
return {
name: 'require-summary',
async page(slug, { outline, ctx }): Promise<Finding[]> {
const resolved = await outline.resolve(slug, ctx);
if (!resolved) return [];
const entry = await resolved.collection.get(resolved.record.entryId, ctx);
if (!entry || typeof entry.data.summary === 'string') return [];
return [{
check: 'require-summary',
severity: 'error',
slug,
message: `'${slug}' has no summary — the docs index and llms.txt read it.`
}];
}
};
}import { links } from '@ogygia/content';
import { requireSummary } from '$lib/checks';
export const docs = site({ outline: docs, checks: [links(), requireSummary()] });Severity decides what happens. 'error' throws from load, which fails the build at prerender and blocks the page in dev, while 'warn' shows up in the docs.check() findings and never blocks a render.
Checks vs. structural build errors
These are two deliberately separate layers. Structural invariants (an orphan entry, a slug collision, an inconsistent NN- sequence) are outline build errors that are always on and cannot be configured away, because a site that has them is not well-formed. Checks are content policy: pluggable, per site and set by severity. links() is policy, so whether a deliberately dead link during a migration is allowed, tolerated as warn or forbidden is your decision.
Do / don’t
- Do run
links()from the start. It costs nothing and makes every reorganization safe. - Do assert on
docs.check()in CI for a dynamic site, where it stands in for the structural check the prerender crawler would otherwise give you. - Don’t treat a finding as noise. A broken link or a missing anchor is a real defect that a reader would run into, so fix the link or the target.
- Don’t wait for production to find a dead cross-reference, when the build and your dev server can tell you first.
Next chapter: the router makes navigation feel like an app, with one request per navigation, no reload and no waterfall.