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.

Margins come from render options, not CSS. A 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/.otf files 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 with pdfReady:
<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 with 504 — 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.