# Vorlagensprache verstehen und anpassen

> Wie die Platzhalter, Bedingungen, Schleifen und Helfer in Druckvorlagen funktionieren, welche Daten dir zur Verfügung stehen und wie du eine bestehende Vorlage gefahrlos änderst.

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](/docs/merchants/basics/print-templates/delivery-note). Dieser Artikel hier geht ausschließlich um den Quellcode.

## Wo du die Vorlagensprache bearbeitest

1. Öffne **Einstellungen → Druckvorlagen** und klicke die Vorlage an.
2. Ö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:

```handlebars
<h1>{{company.name}}</h1>
<p>Bestell-Nr.: #{{order.number}}</p>
```

Verschachtelte Daten erreichst du mit **Punktnotation** — jeder Punkt geht eine Ebene tiefer:

```handlebars
{{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:

```handlebars
{{{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:

```handlebars
{{! 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 von `-5` (niedrigste) bis `5` (höchste), `0` ist mittel |

### `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:

```handlebars
{{#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 …}}`:

```handlebars
{{#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:

```handlebars
{{#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}}`:

```handlebars
{{#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:

```handlebars
{{#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:

```handlebars
{{#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`:

```handlebars
{{#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:

```handlebars
{{#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:

- **`money`** rundet 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}}€`.
- **`date`** nutzt die üblichen Kürzel: `d` Tag, `m` Monat, `Y` vierstelliges Jahr, `H:i` Stunde:Minute. `format="d.m.Y"` ergibt also `05.01.2026`. Ohne Angabe wird `d-m-Y` verwendet.
- **`image`** lä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.
- **`barcode`** erkennt 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 sind `w` und `h` für Breite und Höhe, `showCode="true"` für die Klartextzeile und `color="0,0,0"` für die Farbe als RGB-Werte.
- **`limit`** kürzt zu lange Texte, damit sie eine Tabellenspalte nicht sprengen. `chars` gibt die maximale Länge an (ohne Angabe 50 Zeichen), `end` das Anhängsel für gekürzte Texte — ohne Angabe wird nichts angehängt. Kürzere Texte bleiben unverändert.
- **`font`** gehört in den `<head>` der Vorlage und bettet die Schriftart ein, damit sie im PDF sicher zur Verfügung steht. Verfügbar sind unter anderem `roboto`, `lato`, `opensans`, `montserrat`, `nunito`, `oswald`, `raleway`, `merriweather`, `notosans`, `ptsans`, `sourcesans3`, `ibmplexsans`, `ibmplexserif` und `newsreader`. Ein unbekannter Name fällt automatisch auf `roboto` zurü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:

```handlebars
<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, **kein** `display: flex` und kein `grid`.
- Fußzeilen mit `position: fixed` platzieren, damit sie auf jeder Seite erscheinen.
- Seitenumbrüche über CSS steuern: `page-break-after: always` an einem Element, `page-break-inside: avoid` an 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 |
