von email_templates.xml bis zum MailHog-Test
Transaktionale E-Mails entscheiden darüber, ob Kunden einer Bestellbestätigung, einer Versandmeldung oder einem Zugangslink vertrauen, und in Magento 2 hängt ihre Zuverlässigkeit von einer sauberen Registrierung über email_templates.xml, korrekt genutzten {{var}}- und {{trans}}-Direktiven sowie einer nachvollziehbaren Store-spezifischen Auflösung der E-Mail-Templates ab. Wer eigene Variablen per Plugin injiziert, Header und Footer über Layout-XML pro Store View steuert und lokal mit MailHog testet, baut robuste E-Mail-Templates statt fragiler Einzelfall-Lösungen.
Inhaltsverzeichnis
- 1. Transaktionale E-Mails in Magento: Einordnung und Abgrenzung
- 2. Template-Registrierung über email_templates.xml
- 3. Backend-Override: E-Mail-Templates in Marketing verwalten
- 4. Directive-Syntax: {{var}} und {{trans}} im Template
- 5. Eigene Template-Variablen injizieren
- 6. E-Mail-Layout-XML: Header, Footer und Logo steuern
- 7. Multi-Store und Mehrsprachigkeit: Template-Auflösung
- 8. Transaktionale E-Mails im Vergleich: unsicher gegen empfohlen
- 9. Lokales Testing mit MailHog und Preview-Rendering
- 10. Zusammenfassung
- 11. FAQ
1. Transaktionale E-Mails in Magento: Einordnung und Abgrenzung
Eine transaktionale E-Mail ist in Magento 2 jede Nachricht, die als direkte Reaktion auf eine konkrete Aktion im Shop verschickt wird: Bestellbestätigung, Rechnung, Versandbenachrichtigung, Passwort-Reset oder Kontoaktivierung. Im Unterschied zum Newsletter, der über Magento_Newsletter und eine Abonnentenliste läuft, wird eine transaktionale E-Mail immer über den Magento\Framework\Mail\Template\TransportBuilder zusammengestellt und synchron oder über einen Queue-Consumer verschickt, sobald das auslösende Ereignis eintritt. Die technische Basis dafür ist in jedem Fall ein registriertes E-Mail-Template, keine Inline-Zeichenkette im PHP-Code.
Für Agenturen ist diese Unterscheidung wichtig, weil transaktionale E-Mails andere Anforderungen an Zuverlässigkeit und Nachvollziehbarkeit haben als Marketing-Mails: Eine Bestellbestätigung darf nicht in einer Warteschlange hängen bleiben, ein Passwort-Reset-Link muss innerhalb von Sekunden ankommen. Die folgenden Abschnitte behandeln deshalb ausschliesslich die E-Mail-Ebene selbst, also Registrierung, Direktiven, eigene Variablen, Layout-Steuerung, Store-Auflösung und lokales Testing von E-Mail-Templates, ohne auf die Geschäftslogik von Retouren, Rechnungs-PDFs oder Betrugsprüfung einzugehen.
2. Template-Registrierung über email_templates.xml
Jedes Modul, das eine eigene transaktionale E-Mail ausliefern will, deklariert sie in etc/email_templates.xml. Die Datei folgt dem Schema urn:magento:module:Magento_Email:etc/email_templates.xsd und definiert pro template-Knoten eine eindeutige id, ein Label für das Backend, den Dateinamen der zugehörigen .html-Datei, den Typ (html oder text) sowie die area, für die das Template gilt. Diese id ist der Schlüssel, mit dem später TransportBuilder::setTemplateIdentifier() das Template referenziert, sie taucht ausserdem als Auswahlwert in System-Konfigurationsfeldern vom Typ Magento\Config\Model\Config\Source\Email\Template auf.
Die eigentliche .html-Datei liegt unter view/frontend/email/ oder view/adminhtml/email/ für Backend-Mails und wird beim ersten Zugriff geparst, gerendert und im Template-Cache abgelegt. Wichtig für saubere E-Mail-Templates: Der Dateiname in email_templates.xml muss exakt mit dem physischen Dateinamen übereinstimmen, sonst wirft Magento beim Laden eine LocalizedException statt einer stillen Fehlanzeige. Eine zweite, oft übersehene Regel: Ändert sich nur der Inhalt der .html-Datei, reicht ein Cache-Flush, ein erneutes setup:upgrade ist für reine Textänderungen an bereits registrierten E-Mail-Templates nicht nötig.
<?xml version="1.0"?>
<!--
Registers a custom transactional email template for the frontend area.
The "id" is the unique identifier used later by TransportBuilder::setTemplateIdentifier().
-->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Email:etc/email_templates.xsd">
<template id="mironsoft_orderfollowup_email_template"
label="Mironsoft Order Follow-up Email"
file="order_followup.html"
type="html"
module="Mironsoft_EmailTemplates"
area="frontend"/>
</config>
3. Backend-Override: E-Mail-Templates in Marketing verwalten
Unter Marketing > Communications > E-Mail-Templates kann jeder Administrator ein XML-registriertes Template laden, im WYSIWYG-Editor anpassen und als eigenständige Kopie in der Datenbank speichern. Technisch entsteht dabei eine Zeile in der Tabelle email_template, die über orig_template_code weiterhin auf die ursprüngliche id aus email_templates.xml verweist, aber eigenen Inhalt, eigene Variablen und einen eigenen Titel trägt. Diese datenbankgespeicherte Kopie hat gegenüber dem XML-Template immer Vorrang, sobald sie im relevanten Konfigurationspfad, zum Beispiel Sales Emails > Bestellung > Vorlage für Bestell-E-Mail an Kunden, ausgewählt wurde.
Der Vorteil dieses Mechanismus: Redaktionelle Änderungen an einer transaktionalen E-Mail erfordern keinen Deploy, keinen Code-Cache-Flush und keine Entwicklerressource. Der Nachteil, den Agenturen im Review beachten sollten: Datenbank-Templates werden bei einem Reindex oder Setup-Upgrade nicht automatisch mit neuen Modul-Versionen synchronisiert, sie frieren den Stand zum Zeitpunkt der Anpassung ein. Wer strukturelle Änderungen, etwa neue Variablen oder neue Directive-Logik, an einem bereits im Backend überschriebenen Template vornimmt, muss die Anpassung an beiden Orten pflegen oder die Datenbank-Kopie bewusst zurücksetzen.
4. Directive-Syntax: {{var}} und {{trans}} im Template
Innerhalb einer .html-Datei für E-Mail-Templates verarbeitet Magento den Inhalt nicht als PHP und nicht als reguläres Layout, sondern über Magento\Email\Model\Template\Filter, eine Erweiterung des generischen Template-Filters um E-Mail-spezifische Direktiven. Die Direktive {{var object.getMethod()}} gibt einen Wert direkt aus, wobei object aus den per setTemplateVars() übergebenen Variablen stammt und über Getter-Methoden oder Array-Zugriff ausgelesen werden kann, etwa {{var order.getIncrementId()}} oder {{var customer_name}}. Ohne Escaping-Filter wird der Wert roh eingefügt, weshalb bei nutzergenerierten Inhalten zusätzlich ein Escape-Filter sinnvoll ist.
Die Direktive {{trans "Text mit %platzhalter" platzhalter=$wert}} übernimmt zusätzlich die Übersetzung über die aktive Store-Sprache und erlaubt benannte Platzhalter, die zur Laufzeit ersetzt werden, ähnlich wie __() im PHP-Code, nur innerhalb des Templates. Für strukturelle Wiederverwendung existiert {{template config_path="design/email/header_template"}}, das ein weiteres, in der Systemkonfiguration hinterlegtes Template einbindet, typischerweise für Header und Footer. Wer in einer transaktionalen E-Mail bedingte Inhalte braucht, etwa einen Rabattblock nur bei gesetztem Gutschein, nutzt {{depend var}}...{{/depend}} oder {{if var}}...{{/if}}, beide werden vom selben Filter ausgewertet wie {{var}} und {{trans}}.
{{template config_path="design/email/header_template"}}
<table role="presentation" width="100%" cellpadding="0" cellspacing="0">
<tr>
<td class="email-intro">
<!-- {{trans}} handles translatable strings with named placeholders -->
<p>{{trans "Hello %name," name=$customer_name}}</p>
<p>{{trans "your order #%increment_id was shipped on %date." increment_id=$order.getIncrementId() date=$shipping_date}}</p>
</td>
</tr>
<tr>
<td class="email-tracking">
<!-- {{var}} outputs a raw variable value, optionally through a filter -->
<p><a href="{{var tracking_url}}">{{trans "Track your shipment"}}</a></p>
<p>{{var order.getShippingDescription()}}</p>
</td>
</tr>
</table>
{{template config_path="design/email/footer_template"}}
5. Eigene Template-Variablen injizieren
Der naheliegende, aber unsaubere Weg, zusätzliche Daten in eine transaktionale E-Mail zu bekommen, ist das Kopieren und Anpassen einer Core-Sender-Klasse. Sauberer ist ein Plugin auf Magento\Framework\Mail\Template\TransportBuilder::setTemplateVars, das als around-Plugin die übergebenen Variablen um eigene Werte ergänzt, bevor der Originalaufruf weiterläuft. Das Plugin selbst bekommt per Constructor Property Promotion einen eigenen TemplateVariablesProviderInterface injiziert, eine kleine Service-Contract-Schnittstelle mit einer Methode getVariables(), die pro Anwendungsfall, etwa Bestellung, Kunde oder Angebot, unterschiedlich implementiert werden kann.
Alternativ, wenn eine komplett eigene transaktionale E-Mail von Grund auf gebaut wird, lohnt sich eine dedizierte Sender-Klasse statt eines Plugins auf den generischen TransportBuilder. Diese Klasse injiziert TransportBuilder, den eigenen Variablen-Provider und einen Logger direkt über den Konstruktor, baut Template-Identifier, Template-Optionen (area, store) und Template-Variablen in einer einzigen, verketteten Aufrufkette zusammen und kapselt Fehlerbehandlung an einer Stelle. Der entscheidende Architekturpunkt: Die Variablen-Logik gehört in eine eigene, austauschbare Klasse, nicht in die Sende-Methode selbst, damit sie unabhängig testbar bleibt und bei neuen Anforderungen nicht die Versandlogik anfassen muss.
final class OrderFollowupSender
{
/**
* @param TransportBuilder $transportBuilder Builds the transport with template and variables.
* @param TemplateVariablesProviderInterface $variablesProvider Supplies additional custom variables.
* @param LoggerInterface $logger Logs failed sends without interrupting the caller.
*/
public function __construct(
private readonly TransportBuilder $transportBuilder,
private readonly TemplateVariablesProviderInterface $variablesProvider,
private readonly LoggerInterface $logger,
) {
}
/**
* Sends the order follow-up transactional email for a given order.
*
* @param OrderInterface $order Order the follow-up email refers to.
* @return bool True on success, false if sending failed.
*/
public function send(OrderInterface $order): bool
{
$storeId = (int) $order->getStoreId();
try {
$transport = $this->transportBuilder
->setTemplateIdentifier('mironsoft_orderfollowup_email_template')
->setTemplateOptions(['area' => 'frontend', 'store' => $storeId])
->setTemplateVars(array_merge(
[
'order' => $order,
'customer_name' => $order->getCustomerFirstname(),
],
$this->variablesProvider->getVariables($order)
))
->setFrom('sales')
->addTo((string) $order->getCustomerEmail())
->getTransport();
$transport->sendMessage();
return true;
} catch (\Exception $exception) {
$this->logger->error($exception->getMessage());
return false;
}
}
}
Damit die zusätzlichen Variablen tatsächlich ankommen, muss der Plugin-Typ in di.xml auf den TransportBuilder registriert werden. Ein sortOrder ist hier meist unkritisch, solange kein zweites Plugin dieselbe Methode ebenfalls verändert.
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
<type name="Magento\Framework\Mail\Template\TransportBuilder">
<plugin name="mironsoft_addCustomTemplateVariables"
type="Mironsoft\EmailTemplates\Plugin\AddCustomTemplateVariablesPlugin"
sortOrder="10"/>
</type>
</config>
6. E-Mail-Layout-XML: Header, Footer und Logo steuern
Der Standardweg, Header, Footer und Logo einer transaktionalen E-Mail zu steuern, führt über Stores > Konfiguration > Allgemein > Transaktions-E-Mails, wo pro Store View ein Logo, ein Logo-Alt-Text sowie ein Header- und Footer-Template hinterlegt werden können. Das reicht für einfaches Branding, stösst aber an Grenzen, sobald Header oder Footer komplexere, block-basierte Struktur brauchen, etwa dynamische Social-Media-Icons oder store-spezifische Rechtstexte, die selbst wieder aus mehreren Bausteinen bestehen.
Für diesen Fall lohnt sich eine eigene Layout-XML-Datei im Bereich area="frontend", zum Beispiel view/frontend/layout/email_orderfollowup_header.xml, die Blöcke wie üblich über referenceContainer und block-Knoten definiert. Die Sender-Klasse emuliert dafür kurzzeitig den Frontend-Kontext des jeweiligen Stores, rendert den Layout-Handle, holt sich per getBlock() das gerenderte HTML von Header und Footer und übergibt es als zusätzliche Template-Variable, etwa header_html, an setTemplateVars(). Im .html-Template greift {{var header_html}} den bereits gerenderten Block einfach ab, ohne dass die E-Mail-Direktiven selbst Block-Logik kennen müssen.
Der Vorteil dieses Zusammenspiels aus Layout-XML und E-Mail-Templates: Header und Footer folgen denselben Konventionen wie das restliche Theme, jede Store View kann eigene Blöcke und eigene .phtml-Dateien nutzen, und Änderungen am Layout landen wie gewohnt über den normalen Theme-Fallback von Store View über Website bis Default, statt über eine separate, schwer wartbare E-Mail-Sonderlogik.
<?xml version="1.0"?>
<!-- view/frontend/layout/email_orderfollowup_header.xml -->
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
<body>
<referenceContainer name="content">
<!-- Rendered in area="frontend" per store view during design emulation -->
<block class="Mironsoft\EmailTemplates\Block\Header"
name="email.header"
template="Mironsoft_EmailTemplates::email/header.phtml"/>
<block class="Mironsoft\EmailTemplates\Block\Footer"
name="email.footer"
template="Mironsoft_EmailTemplates::email/footer.phtml"/>
</referenceContainer>
</body>
</page>
7. Multi-Store und Mehrsprachigkeit: Template-Auflösung
Magento löst die tatsächlich verschickte transaktionale E-Mail in einer klaren Reihenfolge auf. Zuerst wird der store-spezifische Konfigurationswert des relevanten System-Config-Pfads gelesen, etwa sales_email/order/template, gescoped auf Store View, Website und Default in dieser Priorität. Zeigt dieser Wert auf eine im Backend gespeicherte Datenbank-Kopie, wird diese verwendet, inklusive ihres eigenen Inhalts und ihrer eigenen Variablen. Zeigt er weiterhin auf den XML-Default, also den in email_templates.xml deklarierten Bezeichner, lädt Magento die physische .html-Datei aus dem aktiven Theme, mit vollem Theme-Fallback bis zum Basis-Theme des Moduls.
Die Sprache der E-Mail-Templates selbst folgt nicht dem Dateisystem-Pfad, sondern der aktiven Store-Locale zur Laufzeit des Sendevorgangs: {{trans}}-Direktiven und __()-Aufrufe im Sender-Code ziehen ihre Übersetzung aus den i18n-CSV-Dateien des Themes beziehungsweise Moduls für genau die Locale, die beim Aufruf von setTemplateOptions(['area' => ..., 'store' => $storeId]) aktiv ist. Deshalb reicht ein einziges .html-Template für mehrere Sprachen aus, solange sämtliche sichtbaren Texte über {{trans}} statt als hartcodierter String im Template stehen. Nur wenn sich Struktur oder Bildsprache zwischen Stores unterscheiden sollen, braucht es tatsächlich unterschiedliche Templates pro Store View, nicht nur unterschiedliche Übersetzungen.
Für Agenturen mit internationalen Shops ist die praktische Konsequenz: Bevor ein zweites, sprachspezifisches Template angelegt wird, lohnt sich die Prüfung, ob nicht schon {{trans}} plus vollständige i18n-Abdeckung ausreicht. Ein zusätzliches Template pro Sprache verdoppelt den Pflegeaufwand bei jeder inhaltlichen Änderung, während eine saubere Übersetzungsdatei zentral bleibt und über den normalen Sprachpaket-Fallback gepflegt wird.
8. Transaktionale E-Mails im Vergleich: unsicher gegen empfohlen
Bei transaktionalen E-Mails entscheidet die gewählte Umsetzung direkt darüber, ob eine E-Mail in allen Clients korrekt aussieht, ob der Checkout bei SMTP-Problemen blockiert und ob Inhalte ohne Deploy pflegbar bleiben. Die folgende Übersicht zeigt verbreitete unsichere Muster neben dem empfohlenen Pattern für dieselbe Aufgabe rund um E-Mail-Templates.
| Aufgabe | Unsicher / Fehleranfällig | Empfohlenes Pattern | Vorteil |
|---|---|---|---|
| Inhalt der E-Mail | HTML-String im PHP-Code zusammengebaut | Registrierung über email_templates.xml | Backend-editierbar, übersetzbar, kein Deploy für Textänderungen |
| Layout im Template | CSS Flexbox/Grid, externe Stylesheets | Table-basiertes, inline-gestyltes Markup | Rendert korrekt in Outlook, Gmail und mobilen Clients |
| Versandzeitpunkt | Synchron im Checkout-Request | Asynchron über Message-Queue-Consumer | Checkout blockiert nicht bei SMTP-Latenz oder -Ausfall |
| Eigene Variablen | If/Else-Ketten direkt im Sender | TemplateVariablesProvider per Plugin/DI | Testbar, erweiterbar, ohne Sender-Klasse anzufassen |
| Multi-Store-Branding | Ein globales Template für alle Stores | Store-spezifische Overrides mit Fallback-Kette | Mehrsprachigkeit und Branding ohne Codeduplikation |
Die Tabelle macht deutlich, dass praktisch jede Zeile denselben Grundgedanken verfolgt: Logik, Darstellung und Versand von E-Mail-Templates gehören in separate, über Service Contracts und Plugins austauschbare Bausteine, nicht in eine einzelne, wachsende Methode. Wer diese Trennung von Anfang an einhält, kann später einzelne Bausteine ersetzen, zum Beispiel synchronen Versand gegen einen Queue-Consumer, ohne das gesamte Template neu zu schreiben.
9. Lokales Testing mit MailHog und Preview-Rendering
Im docker-magento-Setup von Mark Shust läuft standardmässig ein MailHog-Container, der jede vom Container abgehende SMTP-Verbindung abfängt, statt sie tatsächlich zuzustellen. Jede transaktionale E-Mail, die im lokalen Environment über TransportBuilder verschickt wird, landet damit automatisch in der MailHog-Weboberfläche, üblicherweise auf Port 8025, statt im echten Postfach eines Kunden, inklusive Header, Rohtext und gerenderter HTML-Ansicht. Für Agenturen ist das der Standardweg, um den kompletten Versandpfad, von der Trigger-Logik über die Directive-Auswertung bis zum tatsächlichen SMTP-Handshake, ohne Risiko zu prüfen.
Für reines Rendering-Testing ohne jeden Versand bietet die Admin-Oberfläche unter Marketing > Communications > E-Mail-Templates eine Preview-Funktion: Beim Öffnen eines Templates wertet Magento serverseitig alle Direktiven mit Beispieldaten aus und zeigt das Ergebnis direkt im Browser, ohne dass ein Mail-Objekt oder eine Transport-Instanz überhaupt erzeugt wird. Für Templates mit anwendungsspezifischen Variablen, die die generische Preview nicht kennt, etwa eigene, per Plugin injizierte Variablen, lohnt sich ein kleines, selbst geschriebenes bin/magento-Kommando, implementiert über die Symfony Console API und registriert über di.xml als CommandListInterface-Eintrag.
Ein solches Kommando lädt das Template über Magento\Email\Model\TemplateFactory, setzt dieselben Variablen wie der produktive Sender und ruft getProcessedTemplate() auf, statt den Umweg über TransportBuilder und einen tatsächlichen Mail-Transport zu gehen. Das Ergebnis lässt sich als .html-Datei nach var/email-preview/ schreiben und direkt im Browser öffnen, komplett ohne SMTP-Verbindung, ohne MailHog und ohne Risiko, versehentlich eine echte Adresse anzuschreiben. In der Praxis kombiniert man beide Wege: die CLI-Preview für schnelle Iterationen an Variablen und Direktiven, MailHog für den vollständigen End-to-End-Test inklusive Betreffzeile, Absenderadresse und Client-Rendering.
10. Zusammenfassung
Eine belastbare transaktionale E-Mail in Magento 2 entsteht nicht durch einen schnell zusammengebauten String im Sender-Code, sondern durch die konsequente Nutzung der dafür vorgesehenen Bausteine: Registrierung über email_templates.xml, Direktiven wie {{var}} und {{trans}} statt hartcodierter Texte, ein eigener TemplateVariablesProvider statt kopierter Core-Klassen, Layout-XML für block-basierte Header und Footer sowie eine klare Store-spezifische Fallback-Kette für Multi-Store-Betrieb. Jeder dieser Bausteine ist einzeln austauschbar, solange die Verantwortlichkeiten sauber getrennt bleiben.
Lokales Testing mit MailHog und eine eigene Preview-Möglichkeit ohne echten Versand runden den Workflow ab: Änderungen an E-Mail-Templates lassen sich so prüfen, bevor sie überhaupt eine echte Adresse erreichen. Wer diese neun Bausteine konsequent anwendet, baut E-Mail-Templates, die sich pflegen, übersetzen und erweitern lassen, ohne bei jeder Anpassung Risiko für den produktiven Versand einzugehen.
Transaktionale E-Mail-Templates in Magento 2: das Wichtigste auf einen Blick
Registrierung
email_templates.xml deklariert die id, das Label und die area. Die id referenziert TransportBuilder::setTemplateIdentifier() im Sender.
Direktiven
{{var}} gibt Werte aus, {{trans}} übersetzt mit Platzhaltern. Beide werden von Magento\Email\Model\Template\Filter ausgewertet.
Eigene Variablen
Ein around-Plugin auf TransportBuilder::setTemplateVars, gespeist aus einem eigenen TemplateVariablesProviderInterface per DI.
Testing
MailHog fängt jede SMTP-Verbindung lokal ab. Eine CLI-Preview rendert Templates ganz ohne Versand.
11. FAQ: Transaktionale E-Mail-Templates in Magento
1Was unterscheidet eine transaktionale E-Mail von einem Newsletter?
2Wie registriere ich ein neues E-Mail-Template?
3Wie überschreibe ich ein Core-Template ohne Codeänderung?
4{{var}} versus {{trans}}?
5Eigene Variablen ohne Core-Klassen zu kopieren?
6Header, Footer, Logo je Store View?
7Fallback-Kette bei der Template-Auflösung?
8Lokales Testing ohne echten Versand?
9Rendering prüfen ganz ohne Versandversuch?
10Warum Versand über Message Queue statt synchron?
Mironsoft
Magento-2-Entwicklung, E-Mail-Templates und Hyvä-Themes
Transaktionale E-Mails, die zuverlässig ankommen und sauber aussehen?
Wir registrieren E-Mail-Templates sauber über email_templates.xml, injizieren eigene Variablen per Plugin statt Core-Kopien und richten lokales Testing mit MailHog für euer Team ein, damit jede transaktionale E-Mail nachvollziehbar und wartbar bleibt.
Template-Audit
Bestehende E-Mail-Templates, Overrides und Directive-Nutzung prüfen und dokumentieren
Custom Sender
Eigene TransportBuilder-Plugins, Variablen-Provider und Layout-XML für Header/Footer
Testing-Setup
MailHog-Integration und CLI-Preview-Kommandos für risikofreies lokales Testing