Eine Druckvorlage ist eine ganz normale HTML-Seite mit CSS — angereichert um Platzhalter in doppelten geschweiften Klammern. Beim Erzeugen eines Belegs setzt FFN Connect die Platzhalter durch die echten Werte der Bestellung ein und wandelt das fertige HTML anschließend in ein PDF um.
Die Platzhaltersprache heißt Handlebars. Sie ist bewusst klein gehalten: Es gibt Platzhalter, Bedingungen, Schleifen und eine Handvoll Helfer — mehr nicht. Rechnen, Datenbankabfragen oder Programmlogik sind nicht vorgesehen. Alles, was auf dem Beleg stehen soll, muss also entweder fester Text sein oder als Wert in den Daten vorliegen.
Hinweis
Wie du eine Druckvorlage anlegst, eine fertige Vorlage auswählst und sie einem Verkaufskanal zuweist, beschreibt der Artikel Lieferschein-Vorlagen erstellen. Dieser Artikel hier geht ausschließlich um den Quellcode.
Wo du die Vorlagensprache bearbeitest
- Öffne Einstellungen → Druckvorlagen und klicke die Vorlage an.
- Öffne oben rechts das Menü ⋮ und wähle Quellcode bearbeiten.
Der Editor besteht aus drei Bereichen:
- links der Code,
- in der Mitte die Liste Variablen — der komplette Datenbaum, den deine Vorlage nutzen kann,
- rechts die Live-Vorschau als PDF mit Beispieldaten.
Ein Klick auf ein Feld im Variablen-Baum kopiert den Pfad (z. B. outbound.merchantOutboundNumber) in die Zwischenablage. Bei Listen findest du am rechten Rand ein ⋮-Menü mit Copy Loop — das kopiert einen fertigen {{#each …}}-Block. Beim Tippen schlägt der Editor außerdem passende Pfade vor, sobald du einen Punkt setzt.
Wichtig
Solange deine Druckvorlage noch mit einer fertigen Vorlage (Preset) verknüpft ist, kannst du den Code nicht bearbeiten — er wird von der Vorlage verwaltet. Über Quellcode bearbeiten bietet dir FFN Connect dann Trennen & Anpassen an. Damit wird der Code der Vorlage einmalig in deine Druckvorlage kopiert und die Verknüpfung entfernt. Das lässt sich nicht rückgängig machen, und künftige Verbesserungen an der Vorlage erreichen deine Druckvorlage nicht mehr.
Grundbausteine
Werte ausgeben
Ein Wert wird mit doppelten geschweiften Klammern eingesetzt:
<h1>{{company.name}}</h1>
<p>Bestell-Nr.: #{{order.number}}</p>
Verschachtelte Daten erreichst du mit Punktnotation — jeder Punkt geht eine Ebene tiefer:
{{company.address.postal_code}} {{company.address.city}}<br>
{{company.address.country.name}}
Leerzeichen innerhalb der Klammern sind erlaubt: {{ order.number }} und {{order.number}} sind gleichwertig.
Hinweis
Ein Platzhalter auf ein Feld, das es nicht gibt oder das leer ist, erzeugt keinen Fehler — es wird schlicht nichts ausgegeben. Ein Tippfehler im Pfad fällt dir deshalb nur dadurch auf, dass die Stelle im PDF leer bleibt.
HTML in Werten
{{ … }} gibt Werte immer maskiert aus. Steht in einem Feld Müller & Söhne, erscheint im PDF korrekt Müller & Söhne; enthält ein Feld HTML wie <b>fett</b>, erscheint dieser Text sichtbar als <b>fett</b> und wird nicht als Formatierung interpretiert. Das ist Absicht und schützt dein Layout vor kaputten Daten.
Soll HTML aus einem Feld tatsächlich als HTML wirken, nimmst du drei Klammern oder ein kaufmännisches Und:
{{{product.description}}}
{{& product.description}}
Achtung
Nutze die ungeschützte Ausgabe nur bei Feldern, deren Inhalt du selbst pflegst. Enthält ein solches Feld unvollständiges HTML (z. B. ein nicht geschlossenes <div>), verrutscht das gesamte Layout des Belegs.
Kommentare
Kommentare erscheinen nicht im PDF:
{{! Dieser Hinweis ist nur im Editor sichtbar }}
Welche Daten dir zur Verfügung stehen
Jede Vorlage bekommt beim Erzeugen des Belegs denselben Datenbaum. Auf oberster Ebene gibt es diese Bereiche:
| Bereich | Was drinsteckt |
|---|---|
order |
Die Bestellung aus dem Verkaufskanal: Nummern, Lieferadresse, Positionen wie bestellt, Attribute, Tags |
outbound |
Der Warenausgang (Fulfillment-Auftrag): Lieferschein-Nummer, Lager, Datum, die tatsächlich zu versendenden Positionen, Summen und Steueraufteilung |
company |
Deine Firmendaten: Name, Adressen, Logo |
settings |
Die Einstellungen der Vorlage (siehe unten) |
lang |
Das Sprachkürzel der Ausgabe, z. B. de |
Hinweis
Der Datenbaum ist für alle Vorlagentypen — Lieferschein, Rechnung und eigener Beleg — identisch. Was einen Lieferschein von einer Rechnung unterscheidet, ist also allein dein Code, nicht die verfügbaren Daten. Maßgeblich ist immer die Liste Variablen im Editor: Was dort steht, kannst du verwenden.
outbound — der Warenausgang
Für Lieferscheine ist das der wichtigste Bereich, denn hier stehen die Positionen so, wie sie tatsächlich rausgehen.
| Feld | Inhalt |
|---|---|
outbound.merchantOutboundNumber |
Nummer des Warenausgangs in FFN Connect |
outbound.externalNumber |
Nummer im angebundenen System |
outbound.internalNote |
Hinweis an den Fulfiller |
outbound.date |
Datum des Warenausgangs |
outbound.shippingMethod |
Bezeichnung der Versandart |
outbound.shippingAddress |
Lieferadresse (Adressfelder siehe unten) |
outbound.warehouse |
name, fulfiller_name und address des Lagers |
outbound.items |
Liste der Positionen |
outbound.netTotal |
Summe netto |
outbound.taxAmount |
Summe Steuer |
outbound.grossTotal |
Summe brutto |
outbound.shippingCosts |
Versandkosten |
outbound.grandTotal |
Gesamtsumme inkl. Versandkosten |
outbound.vatBreakdown |
Steueraufteilung, je Steuersatz mit rate, basis und total |
Innerhalb einer Schleife über outbound.items stehen dir je Position zur Verfügung:
| Feld | Inhalt |
|---|---|
position |
Laufende Positionsnummer (1, 2, 3 …) |
name |
Artikelbezeichnung |
description |
Artikelbeschreibung |
merchantSku |
Deine SKU |
ean |
EAN/Barcode-Nummer |
quantity |
Menge in diesem Warenausgang |
orderedQuantity |
Ursprünglich bestellte Menge |
note |
Positions-Hinweis |
image |
Bild-Adresse des Artikels |
weight / grossWeight |
Netto- bzw. Bruttogewicht |
netUnitPrice |
Nettobetrag je Stück |
netTotalPrice |
Nettobetrag der Position |
unitTax |
Steuerbetrag je Stück |
taxTotal |
Steuerbetrag der Position |
vatPercentage |
Steuersatz in Prozent |
total |
Gesamtbetrag der Position |
hsCode |
Zolltarifnummer |
countryOfOrigin / countryCodeOfOrigin |
Ursprungsland bzw. dessen Länderkürzel |
order — die Bestellung
| Feld | Inhalt |
|---|---|
order.number |
Bestellnummer |
order.original_number |
Ursprüngliche Nummer im Verkaufskanal |
order.sales_channel |
Verkaufskanal |
order.shipping_method_name |
Versandart laut Bestellung |
order.shipping_address |
Lieferadresse |
order.line_items |
Bestellpositionen mit sku, name, quantity, note, image_preview_url, attributes und dem kompletten Artikel unter product |
order.meta_data |
Zusatzdaten als Liste aus key und value |
order.attributes |
Bestell-Attribute als Liste aus key, type und value |
order.tags |
Liste der Tags |
order.desired_delivery_date |
Wunschliefertermin |
order.priority |
Priorität als Zahl: 1 hoch, 0 mittel, -1 niedrig |
company — deine Firma
company.name, company.logo_url sowie company.address und company.billing_address.
Adressfelder
Alle Adressen (order.shipping_address, outbound.shippingAddress, company.address, company.billing_address, outbound.warehouse.address) haben denselben Aufbau:
fullname, firstname, lastname, company, street, number, address_line1, address_line2, postal_code, city, state, email, phone sowie country mit country.name und country.iso_alpha_2.
Tipp
address_line1 fasst Straße und Hausnummer bereits zusammen — du brauchst street und number also nur, wenn du beides getrennt setzen willst.
Bedingungen
Mit {{#if …}} blendest du einen Abschnitt nur dann ein, wenn ein Wert gefüllt ist. Jeder {{#if}}-Block muss mit {{/if}} geschlossen werden:
{{#if address_line2}}
{{address_line2}}<br>
{{/if}}
Leer, 0, false und „nicht vorhanden" gelten dabei als nicht erfüllt. Optional gibt es {{else}} und {{else if …}}:
{{#if outbound.internalNote}}
<p>Hinweis: {{outbound.internalNote}}</p>
{{else}}
<p>Kein Hinweis hinterlegt.</p>
{{/if}}
{{#unless …}} ist die Umkehrung — der Abschnitt erscheint, wenn der Wert nicht gefüllt ist:
{{#unless order.tags}}Keine Tags vorhanden{{/unless}}
Schleifen
{{#each …}} wiederholt einen Abschnitt für jeden Eintrag einer Liste. Innerhalb der Schleife beziehen sich die Platzhalter direkt auf den aktuellen Eintrag — du schreibst also {{name}}, nicht {{outbound.items.name}}:
{{#each outbound.items}}
<tr>
<td>{{position}}</td>
<td>{{name}}</td>
<td>{{merchantSku}}</td>
<td style="text-align: right;">{{quantity}}</td>
</tr>
{{/each}}
Willst du in der Schleife auf etwas außerhalb zugreifen, stellst du dem Pfad ../ voran. So machen es auch die mitgelieferten Vorlagen, um Spalten je nach Einstellung ein- und auszublenden:
{{#each outbound.items}}
{{#if ../settings.show_column_sku}}
<td>{{merchantSku}}</td>
{{/if}}
{{/each}}
Zusätzlich stehen dir in jeder Schleife diese Hilfswerte zur Verfügung:
| Wert | Bedeutung |
|---|---|
{{@index}} |
Laufende Nummer, beginnend bei 0 |
{{@first}} / {{@last}} |
true beim ersten bzw. letzten Eintrag |
{{@key}} |
Der Schlüssel — bei benannten Listen wie outbound.vatBreakdown der Steuersatz |
Ist die Liste leer, kannst du mit {{else}} einen Ersatztext ausgeben:
{{#each order.tags}}{{.}} {{else}}Keine Tags{{/each}}
Bei einfachen Listen aus Text (z. B. order.tags) gibt {{.}} den aktuellen Eintrag aus.
outbound.vatBreakdown ist keine einfache Liste, sondern nach Steuersatz benannt. Deshalb arbeitest du dort mit {{@key}} oder mit dem Feld rate:
{{#each outbound.vatBreakdown}}
<tr>
<td>{{rate}} %</td>
<td>{{#money basis}}</td>
<td>{{#money total}}</td>
</tr>
{{/each}}
Immer denselben Bereich ansprechen
Wenn du viele Felder aus demselben Ast brauchst, spart {{#with …}} das ständige Wiederholen des Pfades:
{{#with order.shipping_address}}
{{#if company}}{{company}}<br>{{/if}}
{{#if fullname}}{{fullname}}<br>{{/if}}
{{address_line1}}<br>
{{postal_code}} {{city}}<br>
{{country.name}}
{{/with}}
Helfer
Helfer sind kleine Funktionen, die einen Wert aufbereiten. Sie werden mit einem # eingeleitet und stehen dort, wo das Ergebnis erscheinen soll.
| Helfer | Wofür | Beispiel |
|---|---|---|
money |
Zahl mit zwei Nachkommastellen | {{#money outbound.grossTotal}} |
date |
Datum formatieren | {{#date format="d.m.Y" value=outbound.date}} {{/date}} |
image |
Bild aus dem Internet einbetten | <img src="{{#image company.logo_url}}"> |
barcode |
Barcode oder QR-Code erzeugen | {{#barcode value=ean}} |
font |
Schriftart einbetten | {{#font name="roboto"}}{{/font}} |
withLineBreaks |
Zeilenumbrüche aus einem Textfeld als Umbruch ausgeben | {{#withLineBreaks settings.footer_additional_info}} |
replace |
Text im umschlossenen Abschnitt ersetzen | {{#replace find="Straße" replace="Str."}}{{company.address.street}}{{/replace}} |
start |
Sicherstellen, dass ein Text mit einem bestimmten Zeichen beginnt | {{#start value=order.number start="#"}} |
limit |
Langen Text kürzen | {{#limit value=item.name chars=40 end="…"}} |
Wichtig zu wissen:
moneyrundet auf zwei Nachkommastellen und setzt Tausendertrennzeichen — allerdings im englischen Format (1,234.50). Ein Währungszeichen fügt der Helfer nicht hinzu; die mitgelieferten Vorlagen schreiben es bei Bedarf selbst dahinter:{{#money outbound.grossTotal}}€.datenutzt die üblichen Kürzel:dTag,mMonat,Yvierstelliges Jahr,H:iStunde:Minute.format="d.m.Y"ergibt also05.01.2026. Ohne Angabe wirdd-m-Yverwendet.imagelädt das Bild herunter und bettet es direkt in das PDF ein. Das ist zwingend nötig: Ein<img src="https://…">ohne diesen Helfer bleibt im fertigen PDF leer. Unterstützt werden JPEG und PNG; sehr große Bilder werden automatisch verkleinert. Lässt sich ein Bild nicht laden, erscheint ein neutrales Platzhalter-Symbol statt einer Fehlermeldung.barcodeerkennt den Typ automatisch anhand des Werts. Du kannst ihn auch vorgeben, z. B.{{#barcode value=ean type="EAN13"}}oder{{#barcode value=order.number type="QRCODE"}}. Weitere Angaben sindwundhfür Breite und Höhe,showCode="true"für die Klartextzeile undcolor="0,0,0"für die Farbe als RGB-Werte.limitkürzt zu lange Texte, damit sie eine Tabellenspalte nicht sprengen.charsgibt die maximale Länge an (ohne Angabe 50 Zeichen),enddas Anhängsel für gekürzte Texte — ohne Angabe wird nichts angehängt. Kürzere Texte bleiben unverändert.fontgehört in den<head>der Vorlage und bettet die Schriftart ein, damit sie im PDF sicher zur Verfügung steht. Verfügbar sind unter anderemroboto,lato,opensans,montserrat,nunito,oswald,raleway,merriweather,notosans,ptsans,sourcesans3,ibmplexsans,ibmplexserifundnewsreader. Ein unbekannter Name fällt automatisch aufrobotozurück.
Zusätzlich gibt es einige allgemeine Textwerkzeuge: {{#upper …}} und {{#lower …}} für Groß- bzw. Kleinschreibung, {{#capitalize …}} für einen großen Anfangsbuchstaben, {{#truncate feld 40 "…"}} zum Kürzen und {{#default feld "Ersatztext"}} für einen Ersatzwert, wenn ein Feld leer ist.
Achtung
upper und lower funktionieren bei Umlauten nicht zuverlässig (aus Müller wird MüLLER). Für Überschriften erreichst du dasselbe Ergebnis sauberer über CSS: text-transform: uppercase;.
Einstellungen (settings)
Fertige Vorlagen bringen Einstellungen mit — die Ankreuzfelder und Textfelder, die du links neben der Vorschau siehst, etwa Heading, Show Column: SKU oder Footer Additional Info. Jede dieser Einstellungen ist im Code über settings. plus ihren Namen erreichbar:
<h2>{{settings.heading}}</h2>
{{#if settings.show_delivery_address}}
… Lieferanschrift …
{{/if}}
Ankreuzfelder liefern true oder false und werden deshalb typischerweise mit {{#if}} abgefragt; Text- und Textfeld-Einstellungen gibst du direkt aus. Änderst du eine Einstellung, aktualisiert sich die Vorschau sofort.
Hinweis
Die Einstellungen stammen aus der fertigen Vorlage. Löst du dich mit Trennen & Anpassen von ihr, bleiben die zuletzt gesetzten Werte erhalten — {{settings.…}} liefert also weiterhin das, was du eingestellt hattest, und Vorschau und erzeugter Beleg zeigen dasselbe. Neue Einstellungen kommen aber nicht mehr dazu: Ab jetzt änderst du solche Stellen direkt im Code.
Eigene Anpassungen sicher vornehmen
Arbeite immer mit Entwurf und Vorschau. Alles, was du im Code-Editor tippst, landet zunächst nur im Entwurf — der Beleg, der bei echten Bestellungen erzeugt wird, bleibt unverändert. Rechts siehst du sofort, wie sich die Änderung mit Beispieldaten auswirkt. Erst Änderungen veröffentlichen macht den Entwurf produktiv; Änderungen verwerfen stellt den zuletzt veröffentlichten Stand wieder her. Beide Knöpfe erscheinen nur, solange es einen abweichenden Entwurf gibt.
Gehe in kleinen Schritten vor. Ändere eine Sache, sieh in der Vorschau nach, mach weiter. Wenn nach zwanzig Änderungen etwas kaputt ist, hilft nur noch das Verwerfen aller Änderungen.
Achte auf Paare. Jedes {{#if}} braucht ein {{/if}}, jedes {{#each}} ein {{/each}}, jedes {{#with}} ein {{/with}}. Rücke verschachtelte Blöcke sauber ein — fehlende Schlusstags fallen dann sofort auf.
Beachte die Grenzen der PDF-Erzeugung. Der Beleg wird nicht von einem Browser gerendert, sondern von einem PDF-Werkzeug mit eingeschränktem CSS-Umfang. Halte dich deshalb an das, was die mitgelieferten Vorlagen vormachen:
- Layouts über
<table>aufbauen, keindisplay: flexund keingrid. - Fußzeilen mit
position: fixedplatzieren, damit sie auf jeder Seite erscheinen. - Seitenumbrüche über CSS steuern:
page-break-after: alwaysan einem Element,page-break-inside: avoidan Tabellenzeilen,thead { display: table-header-group; }für wiederholte Tabellenköpfe. - Seitenränder über
@page { margin: 2cm; }setzen. - Bilder ausschließlich über den
image-Helfer einbinden.
Was bei einem Fehler passiert. Kommt die Vorlage nicht durch — etwa wegen eines unbekannten Helfers oder eines überzähligen Schlusstags — entsteht trotzdem ein PDF: Es enthält dann nur den Satz „Es ist ein Fehler bei der Generierung des PDFs aufgetreten. Es kann sein, dass du in deiner Vorlage Syntaxfehler hast." Siehst du diesen Satz in der Vorschau, ist im Code etwas nicht in Ordnung. Der zuletzt veröffentlichte Stand ist davon nicht betroffen, solange du den Entwurf nicht veröffentlichst.
Tipp
Bevor du eine veröffentlichte Vorlage stark umbaust, kopiere dir den kompletten Code aus dem Editor in eine Textdatei. Das ist dein Sicherungsnetz — der Editor kennt nur „Entwurf" und „veröffentlicht", keine Versionshistorie.
Häufige Fehler
| Beobachtung | Ursache | Lösung |
|---|---|---|
| Das PDF enthält nur den Satz zum Generierungsfehler | Syntaxfehler: unbekannter Helfer, falsch geschriebener Blockname oder ein Schlusstag ohne passendes Starttag | Zuletzt geänderte Stelle prüfen; im Zweifel Änderungen verwerfen |
| Ein Platzhalter bleibt leer | Pfad falsch geschrieben oder das Feld ist bei dieser Bestellung nicht gefüllt | Pfad aus dem Variablen-Baum kopieren statt abtippen |
Nach einem {{#if}} fehlt der restliche Beleg oder Inhalte erscheinen doppelt |
Fehlendes {{/if}} bzw. {{/each}} — das meldet die Vorlage nicht als Fehler |
Blöcke einrücken und Paare durchzählen |
| HTML aus einem Feld erscheint als sichtbarer Text | {{ … }} schützt die Ausgabe |
Bewusst {{{ … }}} verwenden |
| Ein Bild fehlt im PDF | <img src="https://…"> ohne den image-Helfer, oder kein JPEG/PNG |
{{#image …}} verwenden; Platzhalter-Symbol heißt: Bild nicht ladbar |
Beträge erscheinen als 1,234.50 |
money nutzt das englische Zahlenformat |
So belassen oder die Zahl ohne Helfer ausgeben |
| Eine Spalte lässt sich nicht mehr über die Einstellungen ausblenden | Die Vorlage wurde von der fertigen Vorlage getrennt — es gibt keine Einstellungen mehr | Abfrage {{#if settings.…}} im Code entfernen oder durch feste Entscheidung ersetzen |
| Positionen auf dem Lieferschein passen nicht zur Bestellung | order.line_items zeigt die Bestellung, outbound.items den tatsächlichen Warenausgang |
Für Lieferscheine outbound.items verwenden |
| Änderungen wirken sich nicht auf neue Belege aus | Der Entwurf wurde nicht veröffentlicht | Änderungen veröffentlichen klicken |