Template author guide
This guide is for people who write a Template (a style family such as a letter or a report) or a Template Configuration (the values that adapt a Template to an organisation). It ties together the reference pages and walks through one complete example. Terms are defined in Vocabulary.
Reference pages this guide builds on:
- Packages: package format, field types, typed patterns, the
TeX deny-list, drift guard,
validate - Standard Fields: the Standard Fields
- Metadata: what a Document author can write and how values are layered
- Worker pipeline: what the engine allows, and why
- Images: images and logos
The model in one minute
A Template package declares fields and one or more Document Types
(each with its own Pandoc/LaTeX file). A Configuration package supplies
Fixed Values (a Document cannot change them) and Defaults (a Document
may override them). A Document's Metadata Block sets the rest. For every field
the highest layer wins: Fixed Values, then the Document, then Configuration
Defaults, then manifest defaults and derived values (title, date).
Everything a Document or a Configuration can put into the TeX source goes through a declared field with a type. That is what keeps authors from injecting TeX.
Package layout
templates/mine/
manifest.yaml fields and Document Types
letter.latex one Pandoc template per Document Type
shared/mine.tex shared TeX parts, included with \input{shared/mine.tex}
fonts/ optional; copied to ./fonts/ in every job
assets/ optional; package-relative files
configurations/example/
configuration.yaml
logo.svg
Directory names match ^[a-z0-9][a-z0-9-]*$. A package is self-contained:
nothing is shared between packages, paths stay inside the package (also after
resolving symlinks), and Git LFS pointer stubs are rejected.
manifest.yaml
manifest_version: 1 # must be 1
fields: # each field is declared once
subject:
type: string
description: Subject line.
accent_color:
type: color
pattern: '^[0-9A-Fa-f]{6}$'
document_types:
letter:
template: letter.latex
fields:
- accent_color
- subject:
required: true # override for this Document Type only
A field has a type, and optionally required, description, default,
pattern and (for enum) values. Field names match ^[a-z][a-z0-9_]*$.
Unknown keys anywhere are errors. See Packages.
Field types
| Type | Value | Where it may go in TeX |
|---|---|---|
string | one line of text | text |
text | multi-line text | text |
list | list of strings | text |
date | a date (free text or a YAML date) | text |
bool | true or false | condition ($if$) |
enum | one of values | command or option |
color | checked by pattern | command or option |
asset | package-relative path, pattern | command or option |
Text values are escaped by Pandoc's LaTeX writer. Values in non-text
positions (the argument of \definecolor, \includegraphics,
\setlength, \usepackage, \setmainfont, any option list in square
brackets, and so on) are not escaped for you, so they need a strict pattern.
Typed patterns
colorandassetfields must have apattern;enumfields must havevaluesmade of letters, digits, space,.,_,-.- Any other field used in a non-text position must have a
patterntoo. - A pattern is an RE2 expression matched against the whole value. It must not
match any of a set of hostile probes (backslash, braces,
%,$,&,#,~,^,|, quotes, newlines,.., a leading/)..*and[^ ]+are rejected as too permissive. - Defaults must pass their own type and pattern. An asset value must be a
clean relative path without
...
Prefer the narrowest pattern that does the job. In neutral, the accent
colour is '^[0-9A-Fa-f]{6}$' and the logo is a path ending in .svg.
Pandoc templates
Each Document Type names one Pandoc template (.latex). It uses Pandoc's
variable syntax: $name$, $if(name)$ ... $else$ ... $endif$,
$for(name)$ ... $sep$ ... $endfor$. Partials and applied functions are not
supported, and a variable may never build a control sequence name
(\foo$name$).
Things the Pandoc LaTeX writer needs from a template (taken from the
fidelity research and what neutral
does):
longtable,booktabs,array,calc,etoolboxfor tables, and\tightlistand\pandocboundeddefined (seeshared/neutral.tex);- the Pandoc variable
highlighting-macrosinside$if(...)$, for code; - sectioning commands: a letter class has none, so
neutraldefines them as bold lines; - guard optional fields (
$if(recipient)$) so a missing value does not break the layout.
Fonts
Fonts are loaded by file path with Path=./fonts/, never by name (the
engine cannot build the font name database under paranoid reads; see
Worker pipeline):
\setmainfont{texgyrepagella-regular.otf}[Path=./fonts/,
BoldFont=texgyrepagella-bold.otf, ItalicFont=texgyrepagella-italic.otf,
BoldItalicFont=texgyrepagella-bolditalic.otf]
Ship only fonts whose licence allows redistribution, unmodified, with their
licence text, and list them in THIRD_PARTY_NOTICES. neutral ships TeX Gyre
under the GUST Font License. Do not ship proprietary fonts in a public
repository.
Images and logos
A logo is an asset field. SVG files are converted to vector PDF; raster
formats are limited as described in Images. Text inside an SVG
is drawn with the fonts of the Worker image, not the Template's fonts.
The TeX deny-list
Every TeX file of a Template (.latex, .tex, .sty, .cls, .def, .cfg,
.ltx, .clo) is scanned after comments are removed. These are errors:
- Lua and shell execution (
\directlua,\latelua,\luaexec,\ShellEscape,luacode,minted,pythontex,markdown, ...); - writing files (
\write,\newwrite,\openout,\immediate, ...); - reading files (
\read,\openin,\verbatiminput,\CatchFileDef, ...); - inclusion (
\include,\InputIfFileExists,\@@input, ...); - obfuscation (
^^notation,\catcode,\csname,\scantokens); \inputunless written exactly\input{shared/NAME}for a file in the package's ownshared/directory;\includegraphicswith a literal absolute or..path;- any
.luafile in the package.
The full list is in Packages. It is a safety net against accidents and careless Templates, not a sandbox: the Worker still runs without network or secrets and with shell escape off (Security).
Standard Fields
Reuse the Standard Fields (title, date, language, author, name,
logo, accent_color, letter and report fields) with the listed types so
that Configurations stay portable between Templates. The page
Standard Fields and the machine-readable table
internal/packages/standard_fields.yaml (used by the loader) are kept in step
by a test, and neutral declares its fields with those types. validate
warns, never fails, when a Template uses a Standard Field name with another
type. A Template may declare any other field it likes; font, for example, is
a Standard Field that neutral does not declare.
Configuration packages
manifest_version: 1
defaults: # a Document may override these
document_type: letter
closing: Kind regards
fields: # Fixed Values: a Document cannot change these
name: Example Garden Society
logo: logo.svg
accent_color: "2F6F5E"
When a Configuration is bound to a Template, every value of a declared field
is checked against that field. A Fixed Value the Template does not declare has
no effect and gives a warning, so one Configuration can serve several
Templates. extends is reserved and not supported in v1. See
Packages.
validate and render
Both commands are subcommands of the outprint binary (or the container
image, whose entrypoint it is). The usage is printed by outprint --help.
outprint validate [--service-config FILE]
[--templates-root DIR] [--configs-root DIR]
[--template NAME] [--config NAME]
outprint render --template NAME --config NAME [--title TEXT] [--date YYYY-MM-DD]
[--json-errors] [--templates-root DIR] [--configs-root DIR]
[--timeout DURATION] [--debug-tex-log]
-o OUTPUT.pdf INPUT.md
validate runs the same loader as service startup: manifests, field
types and patterns, the TeX deny-list, the drift guard, path confinement, and
(with --template and --config) the pair checks. All findings are
collected, one per line; errors give exit code 1, warnings alone are success.
task build
bin/outprint validate --templates-root ./templates --configs-root ./configurations
bin/outprint validate --templates-root ./templates --configs-root ./configurations \
--template mine --config example
render runs the real pipeline (Pandoc and LuaLaTeX) on a Markdown file.
It needs the toolchain, so use the image, which is also what the golden tests
use:
docker build -t outprint .
docker run --rm --user "$(id -u):$(id -g)" -v "$PWD:/data" outprint render \
--templates-root /data/templates --configs-root /data/configurations \
--template mine --config example --date 2026-03-02 \
-o /data/out.pdf /data/input.md
--debug-tex-log prints the full TeX log (it may contain document content;
see Worker pipeline); --json-errors prints the JSON error body on
stdout (Metadata).
Drift guard
validate and startup compare the variables a Pandoc template uses with the
manifest: an undeclared variable, or one not listed by the Document Type that
uses it, is an error; a declared field no template uses is a warning.
Pandoc's own variables (body, highlighting-macros, lang, ...) are
ignored. See Packages.
Goldens for Template authors
The repository's golden tests render Markdown fixtures through the real
pipeline inside the pinned image and compare facts and text, never PDF
bytes: pdfinfo and pdffonts facts, and pdftotext -layout output, plus
assertions such as "fonts are embedded", "this string survives", "this table
header repeats on each page".
To cover your Template:
- Add a fixture directory
testdata/fixtures/<topic>/input.md. - Add a case to the table in
internal/golden/golden_test.gonaming the Template and Configuration, and any literal or forbidden strings. - Generate the goldens inside the image only, then review the diff:
task update-goldens(writestestdata/golden/<topic>/), and runtask test-golden.
Not available yet: a per-Document-Type opt-in to compare rasterised pages (PNG diff). The research suggested it as an optional narrow check (test environment research), but it is not implemented: there is no PNG golden and no manifest or fixture switch for it. Today, layout regressions are caught by the text goldens and by the asserted facts only. If you need visual checks, render and look at the PDF; do not rely on a pixel gate that does not exist.
Worked example: a letter Template derived from neutral
Goal: a mine Template with only the letter Document Type and an extra
department field printed in the letter's place line. The example was checked
with validate and render against the image of this repository.
-
Copy the package and remove what you do not need:
cp -R templates/neutral templates/minerm templates/mine/report.latex -
Edit
templates/mine/manifest.yaml: delete thereport:Document Type and the report-only fields (subtitle,version,abstract,toc; otherwisevalidatewarns that they are never used), then declare the new field and list it in the letter:fields:# ...existing fields...department:type: stringdescription: Department shown in the letter's place line.pattern: '^[A-Za-z0-9 .,-]{1,40}$'document_types:letter:template: letter.latexfields:# ...existing entries...- departmentThe field is printed as text, so the pattern is not strictly needed for safety; it keeps the value short and plain.
-
Use it in
templates/mine/letter.latex, next to the subject:\setkomavar{subject}{$subject$}$if(department)$\setkomavar{place}{$department$}$endif$ -
Check it:
go run ./cmd/outprint validate --templates-root ./templates \--configs-root ./configurations --template mine --config exampleRemove the
departmentline from the letter and run it again: the drift guard now warns that the field is never used. Use$department$without listing the field and it is an error. -
Render a Document that sets it. The Metadata Block of a Document is a
yamlblock at its start (Metadata):```yamlsubject: Opening hoursdepartment: Front desk```Our office is open from Monday to Friday.docker run --rm --user "$(id -u):$(id -g)" -v "$PWD:/data" outprint render \--templates-root /data/templates --configs-root /data/configurations \--template mine --config example --date 2026-03-02 \-o /data/out.pdf /data/input.md -
To offer it to Collections, add the pair to
bindingsin the service configuration (Packages) and bake the package into the image or mount it.
Checklist before you publish a Template
validateis clean (no errors; warnings understood).- Fonts load by path, are redistributable, and are listed in
THIRD_PARTY_NOTICESwith their licence text. - Every non-text field has a narrow pattern.
- A golden fixture covers each Document Type.
- No organisation-specific names, logos or addresses in a public repository:
run
task sanitize(Sanitisation gate).