Invoices
Invoices are vellora's canonical use case: a table of line items, currency and date formatting, and a total. This guide shows the templating features you'll lean on.
Template + data
vellora's templating runs before render. You write the document once and bind a plain data object to it.
js
import { writeFileSync } from "node:fs";
import { renderPdf } from "vellora";
const template = `<!DOCTYPE html>
<html>
<head>
<style>
@page { size: A4; margin: 18mm; }
@page { @bottom-center { content: "Page " counter(page) " of " counter(pages); } }
body { font-family: sans-serif; color: #1a1a1a; font-size: 12px; }
h1 { font-size: 22px; margin: 0 0 4px; }
table { width: 100%; border-collapse: collapse; margin-top: 16px; }
thead th { text-align: left; border-bottom: 2px solid #1a1a1a; padding: 6px 4px; }
tbody td { border-bottom: 1px solid #ddd; padding: 6px 4px; }
.right { text-align: right; }
.total td { border-top: 2px solid #1a1a1a; font-weight: bold; }
</style>
</head>
<body>
<h1>{{ seller.name }}</h1>
<p>Invoice {{ invoice.number }} · Issued {{ invoice.date | date("DD/MM/YYYY") }}</p>
<p>Bill to: <strong>{{ customer.name }}</strong><br/>{{ customer.address }}</p>
<table>
<thead>
<tr>
<th>Item</th>
<th class="right">Qty</th>
<th class="right">Unit price</th>
<th class="right">Total</th>
</tr>
</thead>
<tbody>
{% for item in items %}
<tr>
<td>{{ item.name }}</td>
<td class="right">{{ item.qty }}</td>
<td class="right">{{ item.unitPrice | currency("USD") }}</td>
<td class="right">{{ item.total | currency("USD") }}</td>
</tr>
{% endfor %}
<tr class="total">
<td colspan="3" class="right">Total due</td>
<td class="right">{{ total | currency("USD") }}</td>
</tr>
</tbody>
</table>
{% if note %}<p style="margin-top: 16px;">{{ note }}</p>{% endif %}
</body>
</html>`;
const items = [
{ name: "Aluminum bracket A20", qty: 40, unitPrice: 32.5, total: 1300 },
{ name: "Hex bolt M8 (box of 100)", qty: 25, unitPrice: 18.9, total: 472.5 },
];
const data = {
seller: { name: "Acme Components" },
customer: { name: "Model Workshop", address: "123 Flower St" },
invoice: { number: "INV-2026-00417", date: "2026-06-23" },
items,
total: items.reduce((sum, i) => sum + i.total, 0),
note: "Payment due within 15 days. Sample document — fictional data.",
};
const pdf = await renderPdf(template, data, {
metadata: { title: "Invoice INV-2026-00417", creationDate: "2026-06-23T00:00:00.000Z" },
});
writeFileSync("invoice.pdf", pdf);Templating features used here
{{ var }}— interpolation with dotted paths (invoice.number). All output is HTML-escaped.{% for item in items %}…{% endfor %}— loops over arrays.{% if note %}…{% endif %}— conditionals.| currency("USD"),| date("DD/MM/YYYY"),| number— format helpers.
Pagination
When a line-item list is long enough to span pages, the <thead> repeats at the top of each page automatically, and @page paged-media rules (margins, @bottom-center page counters) are honored. See Compatibility for the supported subset.
A complete, runnable version of this example lives in the repository at examples/render-invoice.ts.