Template format (schemaVersion 1)
The machine-readable definition is the JSON Schema. This document explains intent. If the two disagree, the schema wins and this document has a bug.
Reference examples, each with sample data: order confirmation (data): tables across pages, totals, datasheets; FDV documentation (data): cover with QR code, table of contents, one datasheet per product, continuous page numbers.
Units and coordinates
- All lengths are points (1 pt = 1/72 inch = 0.3528 mm). A4 is 595.28 × 841.89 pt.
- Origin is the top-left corner of the containing box; y grows downward.
- Containing box depends on where the element lives:
| Element lives in | Coordinates are relative to | Width available |
|---|---|---|
section.elements |
the section's top-left, inside the page margins | page width − left/right margin |
pageTemplate.header/footer.elements |
the band's top-left, inside the margins | same as above |
pageTemplate.elements (page layer) |
the page's top-left corner (ignores margins) | full page width |
Document structure
Template
├── page size, orientation, margin
├── fonts[] font faces (builtin or uploaded asset)
├── styles{} named text styles; "base" is the default
├── dataSchema optional JSON Schema for render data (data contract)
├── sampleData editor preview data only
└── parts[] rendered in order
├── flow part
│ ├── pageTemplates { default, first?, last? }
│ │ ├── background PDF page drawn behind everything
│ │ ├── header band of fixed height inside top margin area
│ │ ├── footer band of fixed height inside bottom margin area
│ │ └── elements[] page layer (absolute on page)
│ └── sections[] stacked vertically in the flow area
│ └── elements[] positioned inside the section
└── pdf part inserts pages from an existing PDF (optionally once per list item)
The flow area of a page is the page minus margins minus header height minus footer height.
Header sits at the top of the margin box, footer at the bottom of it.
page.margin and table padding are boxes with optional top, right, bottom, left;
an absent side (or an absent box) is 0.
Page template selection per page of a flow part:
- first page of the part →
firstif present - page where the flow ends →
lastif present (wins overfirston a one-page document only iffirstis absent) - otherwise →
default
Because first and last may have different header/footer heights, the flow area can differ
per page. Layout handles this: content is measured against the area of the page it lands on.
Sections
A section is a horizontal band of the flow with a base height. It contains freely positioned
elements. Properties:
| Property | Default | Meaning |
|---|---|---|
repeat |
— | Render the section once per item of a list; item available as as (default item) |
visibleIf |
— | Skip the section when the expression is falsy |
keepTogether |
true |
Never split this section (or each repetition) across pages if it fits on an empty page |
keepWithNext |
false |
Move to the next page together with the following section |
breakBefore |
false |
Always start on a new page |
tocTitle |
— | Adds a table-of-contents entry pointing to where this section starts |
A section grows when it contains a text element with fit: "grow" or a table: an element
moves down by the largest growth (laid-out height minus h) of any element whose bottom edge is
at or above its top, and the section grows by the largest amount any element moved. Elements next
to each other do not push each other. For tables, elements below the table follow wherever the
table ends, even on a later page.
Elements
Common fields: id (unique within the template), type, x, y, w, h, optional name,
rotation, visibleIf, locked; showOn applies only in page templates
(all, first, notFirst, last, notLast).
| Type | Key fields | Notes |
|---|---|---|
text |
content (with {{ }}), style, fit, minFontSize |
fit: clip, ellipsis, shrink (down to minFontSize), grow (sections only) |
image |
source ({asset} or {binding}), fit, align |
Binding may resolve to an asset id, an https:// URL or a data: URI |
rect |
fill, stroke, strokeWidth, radius, ellipse |
|
line |
stroke, strokeWidth, dash |
From (x, y) to (x + w, y + h) |
barcode |
format (qr, code128, ean13, datamatrix), value, color, showText |
Box includes the quiet zone; EAN-13 adds the check digit to 12 digits; showText for 1D codes |
table |
rows, columns[], styles, repeatHeader, carryOver, zebra, borders |
Grows and splits across pages between rows |
toc |
style, leader, links |
Entries from tocTitle on parts and sections |
Table columns
width is points or a fraction ("1fr", "2fr") of the width left after fixed columns.
cell, header and footer are text templates. Inside cell, the row item is in scope under the
name given in rows.as. footer is rendered once after the last row (e.g. a sum).
Styles: body cells use cellStyle, then the column's style on top. Header, footer and
carry-over rows use headerStyle / footerStyle (falling back to cellStyle) with the
alignment of the column's style, so right-aligned number columns get right-aligned headers.
Cells with wrap: true (default) wrap and make the row taller; wrap: false keeps one line with
an ellipsis. Row height = max(minRowHeight, tallest cell + top and bottom padding).
zebra fills every second body row. borders draw horizontal lines under rows, vertical lines
between columns and an outer frame, as enabled (default colour #000000, width 0.5 pt).
@index, @first and @last refer to the row inside cell.
carryOver adds a row at the bottom of each page that breaks the table ("Overføres") and a row
at the top of the next page ("Overført") with running sums for the listed column ids. The label
spans the columns before the first summed column. The running
sum uses the column's cell expression value before formatting, so the cell must compute a number
before its last filter (see Expressions).
Text styles
A style is a set of optional properties. Resolution order (later wins):
styles.base → named style chain via extends → ref in an inline style → inline properties.
Properties still unset after resolution get these defaults: fontFamily = the family of the first
entry in fonts (Inter when fonts is empty), fontSize 10, fontWeight 400, italic false,
underline false, color #000000, align left, verticalAlign top, lineHeight 1.2,
letterSpacing 0.
lineHeight is a multiple of the font size; a text box of height h holds
floor(h / (fontSize × lineHeight)) lines, and fit decides what happens to the rest.
Fonts
fonts declares faces by family, weight and italic. A style's fontFamily must name a
declared family or a bundled builtin family (bundled now: Inter 400/600/700, Noto Sans 400/700). The face is chosen by the CSS weight rule; a missing italic face falls back to upright.
Glyphs missing in the chosen face fall back to the template's other families, then Noto Sans,
then ? (warning missing_glyph).
PDF parts
Insert pages from a PDF given by source. With repeat, one insert per list item (for example one
datasheet per product). pages selects pages ("all", "1-3,5"). fit: scaleToPage scales
pages of another size onto the template page size; keep keeps their own size. countPages
decides whether inserted pages count in @page/@pageCount. overlay is a page template drawn on
top of each inserted page (typically just a footer with a page number); the repeat item is in
scope there. A bound source that is empty inserts nothing. tocTitle adds one entry per insert.
Versioning
schemaVersion is an integer. Additive, optional fields do not change it. Renames, removals,
changed defaults or changed semantics bump it, and the server ships a migration that upgrades
stored drafts and is applied to published versions on read.