Customizing
The core template puts the engine in your project under
.pandoc/. You change it by editing those files and rebuilding the image:
make image # rebuild the engine with your changes
make publish # build your book against it
This is the harder end of Keystone, and the principle here is hard is hard, not impossible. Keystone makes the easy things easy; shipping the whole engine in your project is how it keeps the hard things possible. The work is real — but nothing is hidden or off-limits.
Before changing the engine, check whether you need to. Most new constructs are
shortcuts, not new code — a shortcut composes existing handlers in
shortcuts.yaml and needs no rebuild. Reach into the engine only for genuinely
new behavior. See Writing your own shortcuts.
Adding a handler
When a behavior can't be composed from existing handlers, add one:
- Create a directory under
.pandoc/filters/divs/<class-name>/. The directory name becomes the class —divs/sidenote/handles::: sidenote. - Add
handler.luareturning a per-format hook table (see Handlers for the contract). Implement the formats you need; unhandled formats pass through. - Add optional style files —
macros.texfor PDF,style.cssfor EPUB. If a macro needs a LaTeX package the image doesn't ship, add it when you build the image — see The runtime image. make image, then build your book to confirm it works.- Front it with a shortcut in
shortcuts.yaml— the author-facing name your manuscript uses, and where any required attributes get enforced.
For a step-by-step build of a real handler — concrete function bodies, wiring the style files, validating attributes, and fronting it with a shortcut — follow Writing a handler.
The new class is a shortcut target the moment it exists — no registration needed.
The hook table reaches further than div/span: a handler can also route
Header elements by their class, or intercept whole element types (like
Figure) through global slots. The authoritative contract is the dispatcher
itself — read .pandoc/filters/divs.lua and the existing handlers alongside it
for the complete surface.
Keep each handler to a single concern and let it pass content through in formats
it can't serve — the format integration and
handler pages explain why this is what keeps output correct
everywhere. If you edit filter code, construct and inspect the document through
the engine's AST library (KAST) rather than calling Pandoc's
constructors directly, and read build-time settings through the env-io library
rather than os.getenv; the existing handlers and filters show the pattern.
DOCX/ODT styling is limited here
A docx/odt hook can do plenty on its own — emit raw word-processor markup,
restructure content, or attach a custom-style that reuses a paragraph or
character style already in the reference document. What it can't do from the
template is add a new named style: the reference doc is prebuilt, and the
tooling that regenerates it isn't shipped. So for genuinely new word-processor
styling a custom handler realistically covers PDF and EPUB, while its DOCX/ODT
output emits markup directly or falls back to the default style.
Adding that new style is possible — regenerating the reference documents to carry it is the fork path — but rarely worth it. DOCX and ODT are editable handoff formats for getting a draft to an editor, not publication targets; PDF and EPUB are where styling effort pays off.
Fonts
You don't need to touch the engine to add a font — drop the .otf files in
fonts/ and register them in fonts/fonts-registry.yaml, which works in both
templates. See Typography & fonts. Editing the
engine's built-in font set is a deeper change that belongs to a fork.
The fork boundary
There's a line between using Keystone and forking it. Adding shortcuts,
adding a handler, adjusting a filter for your own book — that's customization,
and core is built for it. Rebuilding the reference documents, changing the
built-in font registry, or reworking the pipeline means you've taken ownership of
the engine itself. That's a legitimate path and core is where it starts — just
know that a fork no longer receives Keystone updates automatically.