Skip to content

How Keystone checks your book

Keystone is a compiler for books, and like any compiler it checks your source as it builds. When something is wrong it tells you — on the terminal, naming the offending element or metadata key — instead of silently letting it through.

This page is the map. Hit a message on the terminal? Find its row below and follow it to a page with the exact message and how to fix it.

Two outcomes: warnings and errors

Every Keystone diagnostic is one of two severities.

  • WARN: — something is off, but the build has a safe fallback. The element is dropped or its bad input ignored, and you still get an artifact — unless you've enabled strict mode, where any warning fails the build.
  • ERROR: — the build cannot proceed correctly, so it stops and writes no completed artifact.

A line behind a │ bar is not Keystone's. It belongs to whatever tool Keystone is reporting for — Pandoc's math converter, the Lua loader, your operating system — and is reproduced exactly as that tool wrote it, so any line numbers or carets it carries still point where they should:

ERROR: 1 equation(s) cannot be converted for epub output
  │ Could not convert TeX math \begin{array}[b]{r} a \\ b \end{array}, rendering as TeX:
  │ \begin{array}[b]{r} a \\ b \end{array}
  │              ^
  │ unexpected "["

Some checks are Pandoc's rather than Keystone's — reading your Markdown, fetching an image, resolving a citation. Their warnings carry the same WARN: prefix as any other, so there is one shape to recognize. The rows below are marked (Pandoc) to tell you which tool to consult if you need to dig, and strict mode fails on them too. When Pandoc stops the build outright it reports in its own words, and Keystone closes with an ERROR: line naming the pass that failed — the ✗ line above it names the stage.

Where checks happen

Your book passes through stages, and each can fail in its own way. They run in the order below — the same order and the same names a build reports as it works (see Watching a build run). The first rows fall before any stage runs, where a build has none to name yet, and the last after every stage has run. Find the row whose message matches yours:

Build stage What can go wrong A message you'll see Severity
Starting the build No pandoc.yaml to read user-metadata.yaml is missing or empty ERROR
Starting the build A target name that doesn't match target metadata file not found: … ERROR
Starting the build A format Keystone doesn't write unsupported format '…' ERROR
Starting the build The project name is still the default the project name is still the default "keystone" WARN
Reading your Markdown (Pandoc) Unclosed div Div at … unclosed … WARN
Reading your Markdown A malformed fence or unclosed span (nothing — silent) —
Rendering hooks A hook that fails hook '…' did not answer … · … returned no body ERROR
Expanding shortcuts Unrecognized class unrecognized class '…' WARN
Expanding shortcuts Unrecognized attribute ignores unrecognized attributes WARN
Expanding shortcuts Missing required field shortcut '…' requires … ERROR
Resolving configuration Missing or invalid metadata the target requires metadata this project does not set · draft-scale must be… ERROR
Resolving configuration Overriding a reserved key overrides Keystone-owned metadata ERROR
Resolving configuration Unknown page size papersize '…' is not a page size Keystone names ERROR
Resolving configuration Page sized twice … and geometry both set the page ERROR
Resolving configuration Geometry auto combined geometry 'auto' cannot be combined with other options ERROR
Resolving configuration Class option belongs to another key classoption '…' sets the page size · … sets the base font size · classoption 'landscape' applies only with geometry: auto ERROR
Resolving configuration Unsupported font size fontsize '…' is not a size this set allows · … is not a font size WARN
Resolving configuration Unrecognized font family fontfamily '…' is not a registry key WARN
Resolving configuration Missing font license font '…' declares no license · font '…' names two licenses that cannot be told apart · font '…' has no license file named … · font '…' license … WARN
Resolving configuration Duplicate font file fonts '…' and '…' both declare '…' · font '…' declares '…' twice · … which differ only in case ERROR
Running handlers Invalid attribute value unrecognized size · unknown type · invalid cols WARN
Running handlers Undeclared mark mark '…' is not declared ERROR
Composing headers & footers Unknown placeholder unrecognized placeholder '{…}' WARN
Composing headers & footers Header/footer text suppressed …-text suppressed WARN
Composing headers & footers An invalid or colliding mark name marks: '…' is not a valid mark name · … collides with the built-in {…} placeholder ERROR
Converting math Math that can't convert equation(s) cannot be converted ERROR
Embedding PDF metadata An unaccepted xmp-metadata value xmp-metadata must be 'auto' or 'disabled' ERROR
Resolving citations (Pandoc) Undefined citation Citeproc: citation … not found WARN
Fetching images (Pandoc) Image not found Could not fetch resource … WARN
Promoting the artifact An artifact that can't be written publishing failed while promoting the artifact to … ERROR

The message column is a representative fragment, not the exact text. Handler warnings in particular share the shape <handler>: <problem> '<value>' — so font: unknown family 'garmond' is the Running handlers row even though its words aren't listed. Match on the stage and the shape, not a literal string.

Finding the offender

A compiler usually points at a file and line number. Keystone can't: it builds on Pandoc, whose Markdown reader doesn't record where each element came from in your source, so there is no line for Keystone to report.

Instead, most messages name the offending element with a compact selector you can search for — its classes, its identifier, and a snippet of its leading text:

WARN: aside: unknown type 'todo'
  (in .aside "A callout whose type is not one of the d…")
  See https://keystone.knight-owl.dev/errors/invalid-value/

That (in …) is your search key. Search your source for the quoted class, the identifier, or the leading-text snippet — in the manuscript for a content mistake, or in shortcuts.yaml and your metadata for a configuration one.

A metadata check is the exception: it has no element to point at, so it names the offending pandoc.yaml key in the message itself (fontsize '13pt' is not a size this set allows) — search for the key.

Strict mode

While you're drafting, a warning shouldn't stop you — you want to see the page even with a placeholder that isn't quite right. So by default warnings are lenient: they print and the build continues.

Before you publish, flip that around. Set KEYSTONE_WARNINGS_AS_ERRORS=true in project.conf and every warning becomes fatal:

ERROR: warnings-as-errors is enabled and the build produced warnings:
  vspace: unrecognized size 'banana'
  No new artifact was written.
  Fix the warnings above, or unset KEYSTONE_WARNINGS_AS_ERRORS for a lenient
  build.
  See https://keystone.knight-owl.dev/errors/#strict-mode

For a one-off check without editing config, make publish strict=true builds once in strict mode — see Publishing your book.

Strict mode fails the build on any warning and promotes no artifact. Where the warning came from decides how you hear about it. The filters collect theirs and report them together after the build, as above, so you fix them in one pass. A warning about your configuration is raised before any filter runs, and stops the build where it is raised, reporting only itself:

WARN: the project name is still the default "keystone"
  Set KEYSTONE_PROJECT, in project.conf or your environment, to name your own
  output.
  See https://keystone.knight-owl.dev/errors/default-project-name/
ERROR: warnings-as-errors is enabled
  The warning above stops the build.
  No new artifact was written.
  Fix the warning above, or unset KEYSTONE_WARNINGS_AS_ERRORS for a lenient
  build.
  See https://keystone.knight-owl.dev/errors/#strict-mode

Pandoc's own warnings turn fatal too, and there the build stops with the pass that failed rather than with a warnings-as-errors message. Draft lenient, publish strict.

Silent by design

Not everything absent is a mistake. A few things are meant to be optional, so their absence is never reported:

  • Optional attribution. An epigraph or pull-quote with no source is a quote without an attribution line — a normal choice, not an error.
  • Undefined conditional symbols. ifdef/ifndef gate on whether a symbol is defined; an undefined symbol is simply false, so the content is omitted. That's the whole mechanism, not a failure. See Conditional content.

Separately, there is a small set of mistakes Keystone cannot see at all — malformed markup that never becomes an element. Know the limits — the mistakes Keystone can't catch.

Under the hood

How the checks are implemented — the closed vocabulary, the shared diagnostic path, and how a core template forker adds their own — is in Validation & diagnostics.