Handlebars templates
An HTML template is a template.html file written with Handlebars
4.7, such as {{invoice.number}}. Its Page header (header.html) and Page footer
(footer.html) are Handlebars too, with the same data, helpers and partials. Excel workbook
templates also use Handlebars; their cell and row helpers are in
spreadsheet templates.
New to templates? Start with build a template.
<h1>Invoice {{invoice.number}}</h1>{{#each lines}} {{component "components/line-row.html" currency=../currency}}{{/each}}<p>Total: {{sum lines "amount"}} {{currency}}</p>Where the data comes from
Section titled “Where the data comes from”- In Studio’s preview, the data is a sample data file, chosen in the Data box of the preview toolbar.
- In production, it is the
datayour application sends with the render request.
What differs from plain Handlebars
Section titled “What differs from plain Handlebars”- A missing value prints nothing and the render goes on. Test optional values with
{{#if}}. - A template whose output is empty fails with
400 BAD_REQUEST(“The template rendered nothing.”) instead of producing a blank PDF. {{> line-row}}looks for a partial file namedline-row,line-row.hbs,line-row.htmlorline-row.handlebarsin the template’s folder, then in apartialsfolder there. See how paths resolve.
Built-in helpers
Section titled “Built-in helpers”Besides the standard Handlebars helpers (if, unless, each, with, lookup, log),
templates have:
| Helper | What it does | Details |
|---|---|---|
qrCode, barcode |
Draw a QR code or barcode from a value. | QR codes and barcodes |
component |
Renders a partial file as a component. | Below |
localize |
Prints a label from the active translation file. | Translations |
pdfFormField |
Adds a field the reader fills in. | Form fields |
pdfCreatePagesGroup, pdfAddPageItem, pdfDest |
Page data and link targets for other templates. | Combine templates |
There are no formatting helpers: format numbers, dates and amounts in
your own helpers; see
translations and formatting. A helper of your own named qrCode,
barcode or one of the pdf… helpers replaces the built-in one. Do not name your own helpers
component or localize: the built-in ones win.
Reuse a block with component
Section titled “Reuse a block with component”{{component "path"}} renders a partial file with the caller’s current data merged with the
arguments you pass:
{{#each lines}} {{component "components/line-row.html" currency=../currency}}{{/each}}- Inside the partial, that merged object is also
@root. - The partial does not see the caller’s loop variables, such as
@index. Pass them as arguments.
To build a whole document from JSON building blocks with no HTML, see component templates.
Write your own helpers
Section titled “Write your own helpers”In Studio, open the template’s helpers.js tab, under the HTML. Every top-level function is a helper:
function sum(items, field) { return items.reduce((total, item) => total + Number(item[field] ?? 0), 0);}<p>Total: {{sum lines "amount"}}</p>- Helpers work as the Handlebars guide
describes: hash arguments, block helpers,
Handlebars.registerHelper,Handlebars.SafeStringand functions onmodule.exportsall work. - Helpers must return their value directly. An
asynchelper, or one that returns a pending promise, fails with “Helper … must return synchronously”. require("moment")formats dates andrequire("bwip-js")draws barcodes. See translations and formatting and QR codes and barcodes.- To share code with other templates, paste in a workspace file with
{#asset ./lib/money.js @encoding=utf8}. The path starts from the file’s own folder.
Prepare data with a script
Section titled “Prepare data with a script”A script is a workspace file with a beforeRender function. It runs before the template and can
change the data, for example to sort lines or compute totals. Use a helper to format one value
where it is printed; use a script when several places need the same computed values.
-
In Studio’s explorer, open the menu of the template’s folder, choose New entity → Script and name it
totals.js. Changereq.datainbeforeRender:totals.js function beforeRender(req, res) {const lines = req.data.lines ?? [];req.data.total = lines.reduce((sum, line) => sum + line.quantity * line.price, 0);} -
Open the template, click Template files (the folder icon in the preview toolbar) and, under Scripts, tick
totals.js. -
Print the new value in the template, such as
<p>Total: {{total}}</p>, and click Update preview. -
Save the template and click Publish. Production renders use the script from this version on.
async function beforeRender(req, res) and the callback form beforeRender(req, res, done)
work too. A script without beforeRender fails with 400 BAD_REQUEST, and one that declares
afterRender fails with 400 UNSUPPORTED_FEATURE.
What a script can read and change
Section titled “What a script can read and change”| Field | Change | Meaning |
|---|---|---|
req.data |
Yes | The render data. Must stay JSON. |
req.context |
Yes | Starts empty. Passes values between scripts; it does not reach the template. |
req.template.name |
No | The template’s name. |
req.template.localization.language |
No | The locale in the template’s page.json. |
req.options.localization.language |
Yes | The language of the render. Set it to render in another language. |
req.template.pdfMeta |
Yes | PDF properties: title, author, subject, creator, producer, keywords and language. |
req.template.pdfPassword |
Yes | Passwords and permissions. Absent when the template has none. |
function beforeRender(req, res) { req.template.pdfMeta = { ...req.template.pdfMeta, title: `Invoice ${req.data.invoice.number}`, };}req.template.pdfMetastarts with the PDF properties inpage.json, keywords joined by commas. What a script sets replaces them, and the request’s ownmetadatawins over both. SettingpdfMeta.languagechanges only the PDF’s language, not the translation file.req.template.pdfPasswordholdspassword,ownerPassword,printing(lowResolutionorhighResolution; absent when printing is not allowed), andmodifying,copying,annotating,fillingForms,contentAccessibilityanddocumentAssemblyastrueorfalse. A script that sets it replaces the template’s passwords entirely, so permissions it leaves out are not granted. The request’s ownencryptionstill wins. See PDF standards and security.- A language a script sets must be a language tag such as
de-CH; anything else fails with400 BAD_REQUEST. How the language picks a translation file is in translations. resand any other change toreqare discarded.
Which scripts run
Section titled “Which scripts run”The button in a script’s header sets when it runs:
| Studio label | Runs |
|---|---|
| Runs where included (the default) | Only for templates that tick it under Template files → Scripts. |
| Runs before renders in this folder | Before every render of a template in the script’s folder or below. |
| Runs before every render | Before every render of an HTML or workbook template in the workspace. |
- “Runs before every render” scripts go first, then folder scripts, each group in name order,
then the template’s own scripts in the order ticked. They share one
req, so a later script sees what earlier ones stored. - Two scripts with the same name, or more scripts than the
limits allow, fail the render with
400 BAD_REQUEST. - Component and spreadsheet (
sheets.json) templates run no scripts and have nohelpers.js. - Editing, adding or rescoping a script changes previews at once, but production renders only after you publish the template again.
Environment values
Section titled “Environment values”Self-hosted installations only. Scripts read per-environment values from process.env. They
come from the SCRIPT_ENV line in the server’s .env
file, as comma-separated NAME=value pairs:
SCRIPT_ENV=ADD_WATERMARK=true,COMPANY_CODE=ACMEprocess.envholds only these values, nothing else from the server. Every workspace’s scripts on the installation can read them, and scripts cannot change them.- Names start with a letter or
_and use letters, digits and_, each once. Values cannot contain commas, and surrounding spaces are trimmed. - Values are always text: compare with
process.env.ADD_WATERMARK === "true". An unset name reads asundefined. - Only scripts see
process.env;helpers.jsand the template do not. Pass a value on throughreq.data. - A changed value applies once the server restarts with it, in previews too. The template needs no new publish.
For a worked example, see different settings per environment.
Sandbox
Section titled “Sandbox”Helpers, scripts and templates run in a sandbox:
- There is no network, file system, timers or browser API, and no npm packages:
requiregives youmomentandbwip-js. Send everything a template needs in the data. - Modern JavaScript syntax and built-ins work, except
Intl. - Variables in
helpers.jsdo not carry over between renders. console.logand theloghelper write to the Profiler tab of Studio’s preview.
Time, memory and size limits are in limits.
When something goes wrong
Section titled “When something goes wrong”A mistake in a script, helper or template fails the preview or render. In Studio, the error
appears in the preview pane and your edits stay. Through the API, it comes in the usual
error envelope. A helper that throws shows its own message after
Handlebars render failed:.
| Status and code | Typical message | What to do |
|---|---|---|
400 BAD_REQUEST |
Invalid Handlebars template: Parse error on line 4… |
Fix the template syntax at the reported line. |
400 BAD_REQUEST |
helpers.js: Unexpected token (3:14) |
Fix the JavaScript syntax in helpers.js. |
400 BAD_REQUEST |
Handlebars render failed: Missing helper: "fmt" |
Define the helper, or fix its name. |
400 BAD_REQUEST |
Script "totals.js": cannot read property… |
Fix the named script. Check it against data shaped like the request’s. |
400 BAD_REQUEST |
The template rendered nothing. |
Check the conditions around your content and the data you sent. |
400 LIMIT_EXCEEDED |
Template execution limit exceeded. |
Look for endless loops, or move heavy work out of the template. |
400 LIMIT_EXCEEDED |
Template memory limit exceeded. |
Send less data, or avoid building large strings and arrays. |
400 UNSUPPORTED_FEATURE |
Script "x.js": afterRender is not supported.… |
Remove afterRender; only beforeRender runs. |
Next steps
Section titled “Next steps”- Build a template: paper, margins, header and footer, fonts and images.
- Translations and formatting: languages, numbers, dates and time zones.
- Render email and SMS text without a stored template.