ERP / App Integration
Use IntentText as the print & report engine inside an existing app (ERP, CRM, billing system) — with one dependency and no document viewer to build. Author a template once in the editor, store it in your database, then one button merges it with a record's data and produces HTML or a PDF.
.it editor ──save .it text──▶ DB: print-template { key, source }
│
record (invoice / report JSON) ───────────┤
▼
parseAndMerge(source, data) → renderHTML / renderPrint
▼
HTML or PDF (the "Print" button)
A runnable reference kit lives in the repo at demo/erp-integration/ — run
pnpm demo:erp. This guide is the same pattern, explained.
Install
npm i @dotit/core # the one and only dependency
Everything below uses three functions from a single helper file you copy into your app
(intenttext-print.mjs in the demo). It works in the browser and Node (same code).
import { parseIntentText, renderHTML, renderPrint } from "@dotit/core";
export function renderDocumentHTML(templateSource, data, opts = {}) {
return renderHTML(parseAndMergeSafe(templateSource, data), { theme: opts.theme || "corporate" });
}
export function renderDocumentPrintHTML(templateSource, data, opts = {}) {
return renderPrint(parseAndMergeSafe(templateSource, data), { theme: opts.theme || "corporate" });
}
(The demo's helper uses parseAndMerge directly and also ships printHTML /
printDocument for the browser print dialog — see The Print button.)
1. Store the template as .it text — not an AST
The portable, diffable, future-proof representation is the .it source string.
parseAndMerge consumes it directly, so storing a parsed tree would only add a
serialize step. Your template collection document is simply:
// db.collection("print-template")
{
key: "invtemplate", // your template id
company: "<companyId>", // per customer/company
theme: "corporate", // corporate | legal | editorial | technical | minimal | …
source: "font: | family: Inter…\ntitle: Invoice {{invoice.number}}\n…"
}
source is exactly what the editor produces. Placeholders are
{{path.to.value}}; tables repeat with each: (below). Style part of a line with an
inline styled span,
e.g. value: [17,325 QAR]{ size: 1.2em; weight: bold } — it prints identically here as
in the editor.
Per-tenant branding goes in style: rules
(v4.3) at the top of the template — house styling declared once, no per-line
repetition, content stays queryable:
style: section | color: #0a7 | weight: 600
style: title | family: Georgia | size: 26pt
Each company's stored template carries its own look; the editor shows the rules as 🎨 chips and applies them live, and core prints them identically.
Saving a template from the editor
The editor works on .it text — take that text and PUT it:
await fetch("/api/print-templates/invtemplate", {
method: "PUT",
headers: { "content-type": "application/json" },
body: JSON.stringify({ company, theme, source: editorText }),
});
// server: db.collection("print-template").updateOne(
// { key: "invtemplate", company }, { $set: { theme, source } }, { upsert: true });
2. The data is your record's JSON
The template's {{…}} paths resolve against whatever object you pass — shape your record
to match the template (or author the template to match your record):
{
"company": { "name": "Jadwal Technology", "email": "…", "vat": "…" },
"customer": { "name": "Acme Corporation", "email": "…" },
"invoice": { "number": "INV-2026-0042", "date": "…", "dueDate": "…", "status": "Unpaid" },
"items": [ { "description": "…", "qty": 1, "unitPrice": "…", "total": "…" } ],
"totals": { "subtotal": "…", "taxRate": 5, "tax": "…", "due": "…" }
}
The table loop — this template line repeats once per element of items, binding each to
item:
| {{item.description}} | {{item.qty}} | {{item.unitPrice}} | {{item.total}} | each: items |
See Templates for the full merge semantics.
3. The Print button
Option A — browser, zero extra deps (recommended)
renderDocumentPrintHTML returns print-ready HTML (@page size/margins, running
header/footer, page numbers); print it via a hidden iframe and the browser's native
dialog → Save as PDF. No PDF library, no server round-trip — it prints exactly what
you see.
async function onPrintInvoice(invoiceId) {
const [template, invoice] = await Promise.all([
fetch(`/api/print-templates/invtemplate`).then((r) => r.json()),
fetch(`/api/invoices/${invoiceId}`).then((r) => r.json()),
]);
printDocument(template.source, invoice, { theme: template.theme }); // merge + print
}
To show the result instead of printing, drop the HTML into an iframe/modal:
const html = renderDocumentHTML(template.source, invoice, { theme: template.theme });
document.querySelector("#preview").srcdoc = html; // <iframe id="preview">
Option B — server, real PDF file (for emailing / archiving)
Use @dotit/pdf — the official opt-in companion for the moments no human is at
a browser (email attachments, compliance archiving, batch statement runs):
npm i @dotit/pdf
npm i puppeteer # or: puppeteer-core + your system Chrome (CHROME_PATH)
The enterprise issue flow in one call — merge → seal (tamper-evident SHA-256) → PDF bytes:
import { issuePDF } from "@dotit/pdf";
import { verifyDocument } from "@dotit/core";
app.post("/api/invoices/:id/send", async (req, res) => {
const template = await db.collection("print-template").findOne({ key: "invtemplate", company });
const invoice = await db.collection("invoices").findOne({ _id: req.params.id });
const { source, hash, at, pdf } = await issuePDF(template.source, invoice, {
signer: "Jadwal Billing", role: "Finance", theme: template.theme,
});
// 1) The sealed .it text is the LEGAL ARTIFACT — store it on the record (a few KB).
// Years later: verifyDocument(source).intact proves it unaltered.
await db.collection("invoices").updateOne(
{ _id: invoice._id },
{ $set: { itSource: source, itHash: hash, issuedAt: at } },
);
// 2) Archive the exact bytes that were sent (object storage), then email.
await s3.putObject({ Key: `invoices/${invoice.number}.pdf`, Body: pdf });
await mailer.send({ attachments: [{ filename: `${invoice.number}.pdf`, content: pdf }] });
res.json({ ok: true, hash });
});
No Chrome in your API process? issueDocument() does the same merge→seal and returns
print-ready html — POST it to a rendering sidecar (e.g. Gotenberg) instead. For batch
runs use createPdfRenderer() (reuses one Chrome). Full API: the @dotit/pdf
README.
Receipts (80mm thermal) and other page sizes
The same pipeline produces POS receipts — just change the template's page: size and
margin. Narrow pages need a small margin (an A4-style 20mm would eat half an 80mm roll);
core defaults narrow pages (≤120mm) to a tight 4mm, but set it explicitly to be sure:
font: | family: ui-monospace, monospace | size: 10pt
page: | size: 80mm auto | margin: 4mm
title: {{company.name}}
summary: Receipt {{invoice.number}}
section: Items
| Item | Qty | Total | each: items |
| {{item.description}} | {{item.qty}} | {{item.total}} |
section: Totals
metric: Total | value: {{totals.total}}
metric: Paid | value: {{totals.paid}}
text: Thank you · {{company.name}}
Keep receipt tables to 2–3 narrow columns so they fit the roll width. size: also
accepts A4, A5, Letter, Legal, or any CSS size (size: 210mm 297mm).
Missing data, totals, and Arabic
- Missing fields print blank, not
{{token}}. The kit merges withmissing: "blank", so an absent optional field (e.g.{{customer.phone}}) renders empty rather than leaking the placeholder onto the document. Pass{ missing: "keep" }only while authoring a template, to see which fields are unfilled. - Totals render as label→value rows.
metric: Subtotal | value: …is a document total line (label left, amount right; aTotal/Balance Duerow is emphasized) — the same as the editor. A metric withtarget:/trend:renders as a dashboard KPI card instead. - Arabic / RTL works out of the box: add
meta: | dir: rtland the document (table column order, totals, running footer) lays out right-to-left. Mix Arabic and Latin freely; numbers and{{invoice.number}}stay correct via the browser's bidi handling. - Untrusted data is safe. Merged values are HTML-escaped, and style-property values
(e.g. a per-tenant
color:) can't break out of thestyleattribute — so invoice data from your database can't inject markup.
Why this is portable
- One package (
@dotit/core) plus one small file you own. - No viewer —
renderHTMLreturns a complete, self-contained HTML document (inline CSS); the print path reuses the browser you already have. - Templates are text — versionable, diffable, editable in the editor, no lock-in.
- Same engine everywhere — the editor, this kit, and the CLI all call the same core, so what you design is what prints (including inline styled spans).
Related
- Web Editor — author templates visually
- Core API —
parseAndMerge,renderHTML,renderPrint - Templates · Style Properties
- Themes — the built-in document themes