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, see 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 read-only starter templates are working examples of everything in this guide, filter them by category, 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.

Use the triple brace in content strings. A CSS string is not HTML, so it never decodes entities: escaped output turns Ito & Sons into a literal &amp; in the printed header. Write {{{company}}} inside margin boxes; keep the double brace everywhere in the body.

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, Ctrl+S saves, Ctrl+Shift+F tidies the pane you are in. The ↗ button opens the PDF full-size in a new tab, and the divider between editor and preview drags (double-click it to even them out again).
  • Let the sample data write your placeholders. Typing {{ offers every path in the sample-data pane: customer.name, items.0.sku, with its type. After {{# it offers only the arrays and objects a section can loop over. Ctrl+Space asks for the list at any point.
  • Tidy re-indents; it never reflows. Whitespace between elements is content in HTML: inserting a line break can add a space that moves your layout. So Tidy only fixes indentation and leaves your line structure, and anything inside <pre>, <script> or <style>, exactly as you wrote it. On the sample-data pane it reformats the JSON.
  • 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.