Skip to main content

Template structure

Every template is a directory loaded from your templates folder (--templates-dir, default templates/). Its name — the directory path — is the name you use in the URL.

Files

FilePurposeRequired
template.<ext>Body template. Extension selects the engine.Yes*
header.<ext>Running page header.No
footer.<ext>Running page footer.No
schema.jsonJSON Schema validating request data.No
params.jsonPDF print options.No
examples/*.jsonNamed example datasets.No
assets/…Static files served to the browser as ./assets/....No
partials/…Reusable template fragments.No

* A directory can instead be a raw template (raw.pdf), a remote template (template.http.hbs), or — under the compositions directory — a composition.

Loading order (which loader wins)

For each directory, loaders are tried in priority order and the first match wins:

  1. File templatetemplate.<ext> (HTML → PDF via a browser).
  2. Raw templateraw.pdf (returned as-is).
  3. Remote templatetemplate.http.hbs (fetched from another service).

Compositions are loaded separately from the compositions directory (--compositions-dir, default compositions/).

Engine priority

When several body files exist, the engine is chosen in this order: Mustache → Handlebars → Go template → static HTML. See Engines.

Nested templates

With --templates-recursive, templates can be nested in subdirectories. A template at templates/invoices/eu/ is named invoices/eu (URL-escaped as invoices%2Feu). All children of a directory must themselves be directories.

Names in URLs

Template names are URL-path components, so escape special characters:

  • nested: invoices/eu/templates/invoices%2Feu/render
  • composition: addressed as composition:<name>/templates/composition%3A<name>/render

Hot reload

With --templates-reload-mode=always, each request re-reads the template from disk, so edits take effect without a restart — ideal for local authoring. In production use the default none for a static, validated-at-startup store.

Previewing the HTML a template produces

GET and POST /_debug/templates/<name>/html — mounted only with --debug-enabled — return the body/header/footer HTML an HTML2PDFTemplate produces, before that HTML is ever handed to the browser: the fastest way to tell whether a bad PDF came from the template engine or from print layout. GET previews with no data; POST takes the render data as the JSON request body (never a query parameter — that would leak render data into trace spans and proxy logs). ?task=N selects a task and works on either verb. A raw or remote template reports has_html: false with a reason instead (there is no HTML stage to show); a composition lists its child tasks and previews one at a time, reporting composition-plan as its own reason — the plan listing is not a template that produces no HTML, it is one asking which task to look at.

GET /_debug/templates/<name>/assets/<path> — same gate — serves one file from the template's assets/ directory, so a consumer that renders the previewed markup resolves the links it carries instead of showing it unstyled. See /spec/swagger.json for the exact request and response shape.

See also: Data validation, PDF params, Assets, Partials.