Starter templates
Two complete templates that use the recipes from Multi-page PDFs. Save one
with Create Template, with the sample values as its variables, then
render it with Create PDF from Template and your own values in
modify.
Invoice
A header with the seller’s name, “Page X of Y” in the footer, a line-item table whose header repeats on every page, totals that stay together, amounts in the customer’s language and currency, and a QR code for the payment link. Long invoices continue onto more pages.
<html style="--seller: '{{seller.name}}'">
<head>
<style>
@page {
size: A4;
margin: 28mm 18mm 22mm;
@top-left { content: var(--seller); font: bold 10pt Helvetica, Arial, sans-serif; }
@top-right { content: "Invoice"; font: 10pt Helvetica, Arial, sans-serif; color: #666; }
@bottom-right {
content: "Page " counter(page) " of " counter(pages);
font: 9pt Helvetica, Arial, sans-serif;
color: #666;
}
}
body { font: 10pt Helvetica, Arial, sans-serif; color: #111; }
.meta { display: flex; justify-content: space-between; margin-bottom: 10mm; }
table { width: 100%; border-collapse: collapse; font: inherit; }
th, td { padding: 6px 4px; border-bottom: 1px solid #e5e5e5; text-align: left; }
th { white-space: nowrap; }
th:last-child, td:last-child { text-align: right; }
tr { break-inside: avoid; }
.totals { width: 70mm; margin: 6mm 0 0 auto; break-inside: avoid; }
.totals td { border: 0; }
.pay { display: flex; gap: 6mm; align-items: center; margin-top: 10mm; break-inside: avoid; }
</style>
</head>
<body class="wait-for-render" data-locale="{{locale}}" data-currency="{{currency}}">
<div class="meta">
<div><strong>Bill to</strong><br>{{customer.name}}<br>{{customer.address}}</div>
<div>Invoice {{number}}<br>Issued {{issued}}<br>Due {{due}}</div>
</div>
<table>
<thead>
<tr><th>Description</th><th>Qty</th><th>Unit price</th><th>Amount</th></tr>
</thead>
<tbody>
{{#each items}}
<tr>
<td>{{this.description}}</td>
<td>{{this.qty}}</td>
<td class="money" data-amount="{{this.unitPrice}}"></td>
<td class="money" data-amount="{{this.amount}}"></td>
</tr>
{{/each}}
</tbody>
</table>
<table class="totals">
<tr><td>Subtotal</td><td class="money" data-amount="{{subtotal}}"></td></tr>
<tr><td>VAT {{vatRate}}%</td><td class="money" data-amount="{{vat}}"></td></tr>
<tr><td><strong>Total</strong></td><td><strong class="money" data-amount="{{total}}"></strong></td></tr>
</table>
<div class="pay">
<div id="qr" data-text="{{payUrl}}"></div>
<div>Scan to pay, or transfer to {{seller.iban}}<br>Reference {{number}}</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js"></script>
<script>
const money = new Intl.NumberFormat(document.body.dataset.locale, {
style: 'currency',
currency: document.body.dataset.currency
});
document.querySelectorAll('.money').forEach((el) => {
el.textContent = money.format(Number(el.dataset.amount));
});
const qr = document.getElementById('qr');
new QRCode(qr, { text: qr.dataset.text, width: 96, height: 96 });
document.body.classList.add('render-complete');
</script>
</body>
</html>Sample values:
{
"locale": "de-DE",
"currency": "EUR",
"number": "2026-1024",
"issued": "5 October 2026",
"due": "4 November 2026",
"seller": { "name": "Acme & Co GmbH", "iban": "DE89 3704 0044 0532 0130 00" },
"customer": { "name": "Globex Ltd", "address": "1 Main Street, Berlin" },
"items": [
{ "description": "Design work", "qty": 10, "unitPrice": 95, "amount": 950 },
{ "description": "Hosting, October", "qty": 1, "unitPrice": 49.9, "amount": 49.9 }
],
"subtotal": 999.9,
"vatRate": 19,
"vat": 189.98,
"total": 1189.88,
"payUrl": "https://pay.example.com/i/2026-1024?amount=1189.88¤cy=EUR"
}subtotal, vat and total are printed as given, so calculate them before you send the
request. The seller’s name must not contain an apostrophe, see
Variables in header and footer text.
Report
A full-bleed cover without header or footer, a summary page with a chart, and one page per finding. The title runs in the header of every page after the cover.
<html style="--title: '{{title}}'">
<head>
<style>
@page {
size: A4;
margin: 25mm 20mm;
@top-left { content: var(--title); font: 9pt Helvetica, Arial, sans-serif; color: #666; }
@bottom-right {
content: "Page " counter(page) " of " counter(pages);
font: 9pt Helvetica, Arial, sans-serif;
color: #666;
}
}
@page cover {
margin: 0;
counter-reset: page 0;
@top-left { content: none; }
@bottom-right { content: none; }
}
body { margin: 0; font: 11pt Helvetica, Arial, sans-serif; color: #111; }
.cover {
page: cover;
height: 297mm;
box-sizing: border-box;
padding: 80mm 20mm 0;
background: #111827;
color: #fff;
}
.cover h1 { font-size: 30pt; margin: 0 0 4mm; }
.finding { break-before: page; }
h2, h3 { break-after: avoid; }
figure, tr { break-inside: avoid; }
figure { margin: 8mm 0; }
.severity { display: inline-block; padding: 1mm 3mm; border-radius: 2mm; background: #fee2e2; }
</style>
</head>
<body class="wait-for-render">
<section class="cover">
<h1>{{title}}</h1>
<p>{{client}} · {{date}}</p>
</section>
<section>
<h2>Summary</h2>
<p>{{summary}}</p>
<figure>
<canvas id="chart" width="640" height="300"></canvas>
</figure>
</section>
{{#each findings}}
<section class="finding">
<h2>{{this.id}} · {{this.title}}</h2>
<p><span class="severity">{{this.severity}}</span></p>
<p>{{this.description}}</p>
<h3>Recommendation</h3>
<p>{{this.recommendation}}</p>
</section>
{{/each}}
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.5.1/chart.umd.min.js"></script>
<script>
const chart = {{json chart}};
new Chart(document.getElementById('chart'), {
type: 'bar',
data: { labels: chart.labels, datasets: [{ label: 'Findings', data: chart.values }] },
options: { animation: false, responsive: false }
});
document.body.classList.add('render-complete');
</script>
</body>
</html>Sample values:
{
"title": "Penetration test report",
"client": "Globex Ltd",
"date": "October 2026",
"summary": "We tested the public web application between 1 and 3 October 2026 and found three issues.",
"chart": { "labels": ["Critical", "High", "Medium", "Low"], "values": [0, 1, 1, 1] },
"findings": [
{
"id": "F1",
"title": "Session cookie without Secure flag",
"severity": "High",
"description": "The session cookie is sent over plain HTTP.",
"recommendation": "Set the Secure and HttpOnly flags on the session cookie."
},
{
"id": "F2",
"title": "Verbose error pages",
"severity": "Medium",
"description": "Stack traces are shown to visitors.",
"recommendation": "Return a generic error page in production."
},
{
"id": "F3",
"title": "Missing security headers",
"severity": "Low",
"description": "Content-Security-Policy is not set.",
"recommendation": "Add a Content-Security-Policy header."
}
]
}