Vorlagensprachen
Jede Vorlage ist in einer von drei Sprachen geschrieben. Sie teilen dieselben Daten, dieselben Helfer und dieselben Einstellungen; nur die Syntax unterscheidet sich. Sie wählen die Sprache, wenn Sie eine Vorlage anlegen, und sie bleibt bei der Vorlage: Sie lässt sich später nicht ändern. Um eine Vorlage in eine andere Sprache zu bringen, legen Sie eine neue an und übertragen das Markup.
Kurzreferenz
| Jinja2-kompatibel | Liquid | Handlebars | |
|---|---|---|---|
| Variable | {{ invoice.number }} | {{ invoice.number }} | {{invoice.number}} |
| Helfer mit Argumenten | {{ total | money("EUR") }} oder {{ money(total, "EUR") }} | {{ total | money: "EUR" }} | {{money total "EUR"}} |
| Helfer mit Optionen | {{ qrcode(url, { size: 120 }) }} | {{ url | qrcode: size: 120 }} | {{qrcode url size=120}} |
| Schleife | {% for l in lines %}…{% endfor %} | {% for l in lines %}…{% endfor %} | {{#each lines}}…{{/each}} |
| Schleifenindex (ab 1) | {{ loop.index }} | {{ forloop.index }} | {{add @index 1}} |
| Bedingung | {% if paid %}…{% elif sent %}…{% else %}…{% endif %} | {% if paid %}…{% elsif sent %}…{% else %}…{% endif %} | {{#if paid}}…{{else if sent}}…{{else}}…{{/if}} |
| Vergleich | {% if total > 1000 %} | {% if total > 1000 %} | {{#if (gt total 1000)}} |
| Variablenzuweisung | {% set vat = total * 0.19 %} | {% assign vat = total | times: 0.19 %} | nicht verfügbar; in den Daten berechnen oder Helfer nutzen |
| Partial einbinden | {% include "footer" %} | {% include "footer" %} | {{> footer}} |
| Kommentar | {# … #} | {% comment %}…{% endcomment %} | {{!-- … --}} |
| HTML in einem Wert escapen | geschieht automatisch | {{ name | escape }} | geschieht automatisch |
| HTML unverändert ausgeben | {{ html | safe }} | geschieht automatisch | {{{html}}} oder {{safe html}} |
Jinja2 und Handlebars escapen jeden Wert, den sie ausgeben. Ein Kundenname, der <script> enthält, erscheint also als Text. Liquid gibt Werte unverändert aus. Hängen Sie in einer Liquid-Vorlage | escape an jeden Wert, der von Personen kommt, die Sie nicht kontrollieren (Namen, Adressen, Freitext), sonst wird dieser Text Teil Ihres Markups. In den beiden anderen Sprachen schalten Sie das Escaping nur für HTML ab, dem Sie vertrauen.
Dieselbe Rechnung dreimal
Daten:
{
"invoice": { "number": "2026-0042", "date": "2026-09-11", "paid": false },
"lines": [
{ "description": "Beratung", "qty": 8, "price": 120 },
{ "description": "Reisekosten", "qty": 1, "price": 180 }
]
}
Jinja2-kompatibel
<h1>Rechnung {{ invoice.number }}</h1>
<p>{{ invoice.date | date("dd.MM.yyyy") }}</p>
<table>
{% for line in lines %}
<tr>
<td>{{ loop.index }}. {{ line.description }}</td>
<td>{{ (line.qty * line.price) | money("EUR") }}</td>
</tr>
{% endfor %}
</table>
{% if not invoice.paid %}
<p class="due">Zahlbar innerhalb von 14 Tagen.</p>
{% endif %}
Liquid
<h1>Rechnung {{ invoice.number | escape }}</h1>
<p>{{ invoice.date | date: "dd.MM.yyyy" }}</p>
<table>
{% for line in lines %}
<tr>
<td>{{ forloop.index }}. {{ line.description | escape }}</td>
<td>{{ line.qty | times: line.price | money: "EUR" }}</td>
</tr>
{% endfor %}
</table>
{% unless invoice.paid %}
<p class="due">Zahlbar innerhalb von 14 Tagen.</p>
{% endunless %}
Handlebars
<h1>Rechnung {{invoice.number}}</h1>
<p>{{date invoice.date "dd.MM.yyyy"}}</p>
<table>
{{#each lines}}
<tr>
<td>{{add @index 1}}. {{description}}</td>
<td>{{money (multiply qty price) "EUR"}}</td>
</tr>
{{/each}}
</table>
{{#unless invoice.paid}}
<p class="due">Zahlbar innerhalb von 14 Tagen.</p>
{{/unless}}
Alle drei drucken dasselbe Dokument: Rechnung 2026-0042, 11.09.2026 und die Positionssummen 960,00 € und 180,00 €, wenn die Locale der Vorlage de-DE ist.
Welche Sprache passt
- Jinja2-kompatibel, wenn Sie Python schreiben oder von apitemplate.io umziehen. Die Syntax ist die ausdrucksstärkste: Arithmetik,
set, Makros und Objektliterale direkt im Ausdruck. Der Importer übernimmt apitemplate.io-Vorlagen unverändert und bildet deren eigene Filter ab. - Liquid, wenn Sie Shopify kennen oder von PDFMonkey umziehen. Liquid ist die restriktivste der drei Sprachen und passt deshalb gut, wenn Personen außerhalb Ihres Teams Vorlagen bearbeiten. Formfeeds Liquid läuft mit strikten Filtern, ein unbekannter Filter ist also ein Fehler und kein leerer String. Denken Sie daran, dass Liquid nicht escapt (siehe oben).
- Handlebars, wenn Sie in JavaScript arbeiten oder von jsreport umziehen. Vorlagen bleiben nah an reinem HTML; Logik läuft über Helfer wie
eq,gtundand.
Wie jede Sprache einen Helfer aufruft
Die Helfer sind in allen drei Sprachen dieselben Funktionen; nur die Übergabe der Argumente unterscheidet sich. Nehmen Sie image(url, { width, height, fit, alt }):
| Sprache | Aufruf | Was der Helfer bekommt |
|---|---|---|
| Jinja2 | {{ image(p, { width: 120, fit: "cover" }) }}, {{ p | image({ width: 120 }) }} oder {{ image(p, width=120, fit="cover") }} | den Wert, dann das Optionsobjekt |
| Liquid | {{ p | image: width: 120, fit: "cover" }} | den Wert vor |, dann alle name: wert in einem Optionsobjekt gesammelt |
| Handlebars | {{image p width=120 fit="cover"}} | die Positionsargumente, dann alle name=wert in einem Optionsobjekt gesammelt |
In Jinja2 funktioniert also jeder Helfer als Funktion und als Filter, und der Wert vor | wird das erste Argument. In Liquid ist ein Helfer immer ein Filter: Ein Helfer, der keine Eingabe braucht, braucht trotzdem eine, wie in {{ '' | pageBreak }}, und Positionsargumente stehen vor den benannten ({{ total | money: "USD" }}). Liquid kann kein Objektliteral schreiben, Optionen sind deshalb immer Paare name: wert. Den Aufruf jedes Helfers in allen drei Sprachen zeigt die Helfer-Referenz.
Fehlende Werte und Fehler
| Situation | Jinja2 | Liquid | Handlebars |
|---|---|---|---|
| Eine Variable oder ein Feld, das nicht in den Daten steht | leerer Text | leerer Text | leerer Text |
Ein Feld von etwas, das fehlt ({{ a.b.c }} ohne a) | leerer Text | leerer Text | leerer Text |
| Ein unbekannter Filter oder Helfer | das Rendering schlägt fehl (template_runtime_error) | die Vorlage kompiliert nicht (template_syntax_error) | das Rendering schlägt fehl (template_runtime_error) |
| Ein Partial, das es nicht gibt | das Rendering schlägt fehl | das Rendering schlägt fehl | das Rendering schlägt fehl |
Helfer bekommen den fehlenden Wert als nichts und entscheiden selbst, was sie ausgeben; Vorsicht bei date, das bei einem fehlenden, null- oder leeren Wert das heutige Datum ausgibt. Setzen Sie ein optionales Datum deshalb in eine Bedingung. Ein fehlender Wert hält kein Rendering an. Prüfen Sie Vorlagen deshalb gegen Ihre Datensätze: Der Editor und formfeed validate warnen bei jeder Variablen, die der angezeigte Datensatz nicht enthält. Nutzen Sie default für einen Wert, der berechtigt fehlen darf (siehe Daten).
Unterschiede, die Sie kennen sollten
Jinja2
Die Jinja2-kompatible Engine ist Nunjucks, das in der Browser-Vorschau und auf dem Server gleich läuft. Fast die gesamte Jinja2-Syntax funktioniert. Ausnahmen sind die Teile, die es nur in Python gibt:
- Stringformatierung mit
"%.2f" % valuefunktioniert nicht; sie gibtNaNaus. Nutzen Sienumber(2)odermoney, oder"{:,.2f}".format(value). loop.cycle()gibt es nicht: Nutzen Sieloop.index % 2odercycler():{% set row = cycler("odd", "even") %}und in der Schleife{{ row.next() }}.- Tests brauchen Klammern:
is divisibleby(3), nichtis divisibleby 3, das ist ein Syntaxfehler. Den Testsequencegibt es nicht (nutzen Sieis iterable), undsameasvergleicht Werte wie==statt der Identität. - Die Blöcke
macro,call,filterundautoescapefolgen der Semantik von Nunjucks, die sich in Details von Python-Jinja2 unterscheidet. Prüfen Sie ihre Ausgabe in der Vorschau. - Ein
{% set %}innerhalb einer Schleife ändert eine Variable, die vor der Schleife gesetzt wurde, und der neue Wert bleibt nach der Schleife erhalten; Python-Jinja2 behält den äußeren Wert. Eine Variable, die erst in der Schleife gesetzt wird, ist danach wie in Python verschwunden.
Gängige String-Methoden (.upper(), .replace(), .split(), .strip(), .format() mit Python-Formatangaben wie "{:,.2f}") funktionieren, ebenso namespace() mit {% set ns.total = … %}, True, False und None sowie die Jinja2-Argumente von truncate, sum(attribute=…), map(attribute=…), selectattr und rejectattr. Eine direkt ausgegebene Liste oder ein Dict sieht aus, wie Python sie ausgibt ([1, 2], {'a': 1}).
Liquid
- Die Ausgabe wird nicht escapt. Nutzen Sie
| escapefür Werte von außen, wie oben beschrieben. - Datumsangaben nutzen Tokens von date-fns, nicht
strftime:{{ invoice.date | date: "dd.MM.yyyy" }}. Ein Shopify-Format wie"%d.%m.%Y"ergibt Unsinn. Die Tokens stehen beim Helferdatein der Helfer-Referenz. - Arithmetik läuft über Filter:
{{ qty | times: price }},plus,minus,divided_by. Es sind Formfeedsadd,subtract,multiplyunddivide,7 | divided_by: 2ergibt also3.5und nicht3; ergänzen Sie| floor, wenn Sie die ganze Zahl brauchen. - Filter laufen im strikten Modus: Ein unbekannter Filter verhindert, dass die Vorlage kompiliert, statt einen leeren String auszugeben.
- Benannte Argumente werden ein Optionsobjekt.
| image: width: 120, fit: "cover"erreicht den Helfer als{ width: 120, fit: "cover" }, genau das, was das Objektliteral in Jinja2 undwidth=120in Handlebars übergeben. - Ein Filter lässt sich nicht in einer Bedingung nutzen. Weisen Sie das Ergebnis zuerst zu:
{% assign big = total | gt: 1000 %}{% if big %}…{% endif %}; für einfache Vergleiche schreiben Sie{% if total > 1000 %}. - Neben Formfeeds Helfern funktionieren diese Standardfilter von Liquid so, wie Shopify sie beschreibt:
abs,append,at_least,at_most,capitalize,ceil,compact,concat,escape,escape_once,find,find_exp,find_index,floor,group_by_exp,has,lstrip,modulo,newline_to_br,prepend,reject,reject_exp,remove,remove_first,replace_first,reverse,rstrip,slice,sort_natural,strip,strip_html,strip_newlines,truncatewords,uniq,url_decode,url_encodeundwhere_exp. Diese Namen gehören dagegen Formfeed-Helfern, es läuft also der Helfer, der sich anders verhalten kann als bei Shopify:date,default,divided_by,downcase,first,group_by,join,json,last,map,minus,plus,raw,replace,round,size,sort,split,sum,times,truncate,upcaseundwhere.
Handlebars
- Handlebars hat keine Operatoren für Arithmetik oder Vergleiche. Nutzen Sie die Helfer
add,subtract,multiplyunddivideund die Helfereq,ne,gt,gte,lt,lte,and,orundnotals Teilausdrücke in Klammern:{{money (multiply qty price)}},{{#if (gt total 1000)}}. - Innerhalb von
{{#each}}und{{#with}}beziehen sich die Namen auf das aktuelle Element ({{description}}). Die äußeren Daten erreichen Sie mit../({{../currency}}) oder von der Wurzel aus mit@root({{@root.invoice.number}});{{#each lines as |line i|}}benennt Element und Index. - Hat ein Feld denselben Namen wie ein Helfer und erwähnen Sie es ohne Argumente (
{{date}},{{number}}), gibt Formfeed das Feld des aktuellen Elements aus. Mit Argumenten ({{date invoice.date "P"}}) ist es der Helfer. {{> footer}}rendert das Partial mit dem aktuellen Kontext.
Partials
Gemeinsame Fragmente werden mit {% include "footer" %} (Jinja2 und Liquid) oder {{> footer}} (Handlebars) eingebunden. Ein Liquid-Partial, das stattdessen mit {% render "footer" %} eingebunden wird, sieht nur brand und die übergebenen Werte ({% render "footer", invoice: invoice %}), nicht die Daten der Vorlage; {% include "footer" %} sieht alle. In Jinja2 laden {% import "macros" as m %} und {% from "macros" import price %} Makros aus einem Partial, eine Sammlung von Makros kann also in einem Partial liegen und mehreren Vorlagen dienen.
formfeed templates push speichert die Partials, die eine Vorlage einbindet oder importiert, mit ihrer Version, sodass API-Renderings und der Editor sie auflösen; siehe Partials. Geteilte Partials der Organisation binden Sie mit derselben Syntax ein; eine Vorlage findet nur die, die für ihre eigene Sprache geschrieben sind.
Jede Sprache sieht außerdem das Brand Kit der Organisation als brand: {{ brand.legal_footer }} in Jinja2 und Liquid, {{brand.legal_footer}} in Handlebars. Ein Feld brand in Ihren Daten tritt an seine Stelle.
Helfer
Formatierung, Text, Sammlungen, Logik, QR- und Barcodes, Diagramme und Bilder funktionieren in allen drei Sprachen gleich. Sie stehen mit ihren Signaturen und einem Beispiel je Sprache in der Helfer-Referenz.