Template Guide
Everything you need to design PDF templates for kimidoc. For rendering endpoints and options, see the web app's API reference; for server operation, the admin guide.
Template basics
A kimidoc template is a complete HTML document — CSS and all — with {{placeholders}} where your data goes. The rendering engine is a real Chromium, so anything it can lay out for print, you can use: flexbox, grid, SVG, web fonts, canvas.
Each stored template carries three parts:
- HTML — the document with placeholders.
- Sample data — an example JSON payload. It powers the editor's live preview and doubles as living documentation of the shape your API calls must send.
- Render options — page size, margins, and behavior flags, editable in the editor's page-setup panel.
The six read-only starter templates are working examples of everything in this guide — open one with Use as base and read its source.
Template language
kimidoc uses a mustache subset. Values are HTML-escaped by default; sections handle conditionals and loops.
{{name}} variable, HTML-escaped
{{customer.address.city}} dotted paths
{{items.0.sku}} numeric array indexing
{{{html}}} raw, UNescaped (trusted HTML only)
{{#items}} section: repeats for each array item,
<tr><td>{{sku}}</td><td>{{qty}}</td></tr> fields resolve inside the item
{{/items}}
{{#discount}}…{{/discount}} also a conditional: renders when truthy
{{^discount}}No discount applied.{{/discount}} inverted: renders when falsy/empty
{{.}} the current item itself (arrays of strings)
{{! note to self}} comment, produces no output
Falsy means: missing, null, false, "", or an empty array. Not supported (by design, to stay predictable): partials, custom delimiters, and lambdas.
Page size & margins
Set page geometry in render options (the editor's page-setup panel, or the options object in API calls): a named pageSize (A3–A6, Letter, Legal) or an exact pageSizeMm for anything else — the thermal-receipt starter is 80×200 mm. marginsMm takes one number or per-side values.
margin: declaration inside @page is ignored by the print engine (it only affects browser print preview). Use marginsMm — the business-report starter shows the pattern.A template can pin its own paper size in CSS — @page { size: A5 landscape; } — and that overrides the pageSize/landscape options. Use it when the design only makes sense at one size (certificates, tickets); otherwise leave sizing to options so callers can choose.
Work in print units — pt for type, mm for boxes — rather than px, and your CSS reads like the printed result.
Running headers & page numbers
Native CSS Paged Media — no library, no options needed. Margin boxes repeat on every page and page counters just work:
@page {
@top-left { content: "{{company}} — Confidential"; font-size: 8pt; color: #94a3b8; }
@bottom-center { content: "Page " counter(page) " of " counter(pages); font-size: 8.5pt; }
}
@page :first { /* cover page: suppress the header */
@top-left { content: none; }
}
Margin boxes are drawn inside the page margins, so leave room: a 20 mm+ top margin fits a comfortable running header. Placeholders work inside content strings — they're substituted before the page is laid out. Available boxes: @top-left/-center/-right, @bottom-left/-center/-right.
The pagedMedia option (Paged.js polyfill) exists for engines without native support and is usually unnecessary — don't reach for it unless you know you need it.
Page breaks
.chapter { break-before: page; } /* always start on a new page */
.kpi-row { break-inside: avoid; } /* never split this box */
h2 { break-after: avoid; } /* keep headings with their content */
table { border-collapse: collapse; }
thead { display: table-header-group; } /* repeat header row on each page */
Long tables get page-spanning behavior for free; display: table-header-group on thead repeats column headers after each break. For cards and figures, break-inside: avoid prevents ugly splits.
Images & assets
Upload once, reference everywhere. Upload files to the server's asset store (POST /v1/assets?name=logo.png — see the API reference) and reference them from any template. Assets are served to renders from local storage, so they work even with remote fetches disabled:
<img src="asset://logo.png" style="width:40mm">
For one-off images, data URIs keep the template fully self-contained:
<img src="data:image/png;base64,iVBORw0KGgo…" style="width:40mm">
<img src="{{logoDataUri}}"> <!-- or pass it in your data -->
SVG shines in PDFs — vector output stays sharp at any zoom. Inline <svg> directly for charts, barcodes and logos (the receipt starter draws its barcode this way).
Remote URLs (https://…) work only when the operator has enabled remote fetches; internal/private addresses are always blocked. If a render comes back missing assets, check the X-Kimidoc-Blocked-Remote response header, or watch for the blocked-fetch toast in the editor.
Fonts
- Bundled: Noto families cover Latin, CJK, Indic scripts, and emoji out of the box. Stack them last:
font-family: "Segoe UI", "Noto Sans", sans-serif. - Server-installed: operators can drop
.ttf/.otffiles into the server's font directory (see the admin guide) — reference them by family name. - Per-template fonts: upload the font file as an asset and reference it — no server restart needed:
@font-face { font-family: Corp; src: url("asset://corp.woff2"); } - Webfonts /
@font-face: alternatively inline the font as a data URI, or load remotely (subject to the remote-fetch policy). Fonts load asynchronously — combine withpdfReady:
<script> document.fonts.ready.then(() => window.pdfReady()); </script>
…and render with waitForReady, so printing waits until every glyph is in place.
Charts & async content
Templates can run JavaScript — draw charts on a canvas, compute totals, format numbers. If any of it is asynchronous, tell kimidoc when you're done:
<script>
drawChart(document.getElementById("chart"), {{{chartData}}});
window.pdfReady(); // signal: safe to print now
</script>
Then check Wait for pdfReady() in the editor's page-setup panel (or send waitForReady: true). Two rules:
- If the option is on but the template never calls
window.pdfReady(), the render times out with504— the editor warns you about this. - Purely static templates don't need any of this; rendering waits for load automatically.
Workflow tips
- Design in the editor. Ctrl+↵ re-renders the preview; the ↗ button opens the PDF full-size in a new tab.
- Keep sample data honest. It's the contract your API callers see when they open the template — include every field the template uses, with realistic values.
- Start from a starter. Each one demonstrates a technique: invoice (tables & totals), business report (running headers, page numbers, KPI cards), certificate (landscape, CSS-pinned size), receipt (custom paper width, SVG barcode).
- Automate with the ID. The copy button on a template card gives you its ID for
POST /v1/templates/{id}/render— full request examples are in the API reference. - Test edge cases: long names, many line items, empty arrays — sections (
{{#}}/{{^}}) should handle them gracefully.