Email Renderer
Das Paket @owlat/email-renderer wandelt JSON-Blöcke aus dem Editor in produktionsreife HTML-E-Mails um — mit Client-übergreifender Kompatibilität, CSS-Inlining, Dark Mode und VML-Fallbacks für Outlook.
Das Paket @owlat/email-renderer wandelt JSON-Blöcke aus dem Editor in produktionsreife HTML-E-Mails um — mit Client-übergreifender Kompatibilität, CSS-Inlining, Unterstützung für den Dark Mode und VML-Fallbacks für Outlook.
Schnellstart
import { renderEmailHtml } from '@owlat/email-renderer';
import type { EditorBlock } from '@owlat/shared';
const blocks: EditorBlock[] = [
{
id: 'block-1',
type: 'text',
content: {
html: '<p>Hello {{firstName}}</p>',
blockType: 'paragraph',
fontSize: 16,
textColor: '#333333',
},
},
];
const html = renderEmailHtml(blocks, {
theme: { primaryColor: '#4f46e5' },
preheaderText: 'Your weekly update is here',
inlineCss: true,
});
Render-Pipeline
EditorBlock[]
│
▼ Conditional content filtering (variable-based show/hide)
▼ Theme defaults applied (heading styles, button styles, body text)
▼ Mobile font size rules collected
│
▼ Per-block rendering (table-based HTML with VML fallbacks)
│
▼ Document wrapping (DOCTYPE, head, styles, boilerplate)
▼ CSS inlining (styles onto elements for Gmail/Yahoo)
▼ Optional HTML minification
│
▼ Final HTML string
Render-Optionen
Alle Optionen werden als zweites Argument an renderEmailHtml() übergeben:
| Option | Typ | Standard | Beschreibung |
|---|---|---|---|
theme | EmailTheme | siehe unten | Design-Tokens (Farben, Schriften, Abstände) |
darkMode | boolean | false | Rendering der Dark-Mode-Vorschau aktivieren |
preheaderText | string | '' | Verborgener Vorschautext im Posteingang |
title | string | '' | Dokumenttitel (Browser-Tab) |
baseWidth | number | 600 | Inhaltsbreite in px (typisch 500–700) |
breakpoint | number | 480 | Responsiver Mobil-Breakpoint in px |
direction | 'ltr' | 'rtl' | 'ltr' | Textrichtung für RTL-Sprachen |
lang | string | 'en' | HTML-Attribut lang |
inlineCss | boolean | true | CSS für Gmail/Yahoo inline in die Elemente schreiben |
minify | boolean | false | Ausgabe-HTML minifizieren (MSO-Kommentare bleiben erhalten) |
variableType | VariableType | 'personalization' | Rendermodus für Variablen |
variableValues | Record<string, string> | {} | Werte für die Auswertung bedingter Inhalte |
fontUrls | string[] | [] | URLs von Webfonts, die importiert werden sollen |
customCss | string | '' | Eigenes CSS, das in den Style-Block eingefügt wird |
linkTransform | Function | — | Alle Link-URLs transformieren (UTM, Klick-Tracking) |
onWarning | Function | — | Callback für nicht fatale Render-Warnungen |
targetClient | TargetClient | — | Rendering für einen bestimmten E-Mail-Client simulieren (gmail, outlookDesktop, outlookNew, appleMail, yahooMail) |
validationLevel | ValidationLevel | 'soft' | Strenge der Validierung: skip (keine), soft (warnen), strict (Fehler werfen) |
gmailAnnotations | GmailAnnotations | — | Annotationen für den Gmail-Werbetab (Schema.org-JSON-LD für Rich Cards) |
EmailTheme
interface EmailTheme {
primaryColor?: string;
fontFamily?: string;
backgroundColor?: string;
headingFontFamily?: string;
bodyFontSize?: number;
bodyTextColor?: string;
linkColor?: string;
borderRadius?: number;
spacingUnit?: number;
buttonDefaults?: {
backgroundColor?: string;
textColor?: string;
borderRadius?: number;
fontSize?: number;
fontFamily?: string;
fontWeight?: number;
paddingX?: number;
paddingY?: number;
};
headingDefaults?: {
h1?: HeadingStyle;
h2?: HeadingStyle;
h3?: HeadingStyle;
};
blockDefaults?: Partial<Record<BlockType, Record<string, unknown>>>;
darkModeBackgroundColor?: string; // default '#121212'
darkModeTextColor?: string; // default '#e4e4e7'
darkModeLinkColor?: string; // default '#93c5fd'
baseWidth?: number; // default 600 (min 400, max 800)
}
Theme-Standardwerte werden mit den Eigenschaften auf Blockebene zusammengeführt. Werte auf Blockebene haben immer Vorrang.
Das Feld blockDefaults funktioniert wie mj-attributes in MJML — damit setzen Sie Standardeigenschaften für beliebige Blocktypen (z. B. Standard-Padding für alle Textblöcke, Standard-Border-Radius für alle Buttons). Diese werden vor dem Rendern flach in den Blockinhalt gemischt.
Blocktypen
Der Renderer unterstützt 17 Blocktypen:
Inhaltsblöcke
| Typ | Beschreibung |
|---|---|
text | Rich Text (Absatz, h1, h2, h3) mit Schriftgröße, Farbe, Ausrichtung, Zeilenhöhe |
image | Responsives Bild mit Alt-Text, Link, srcset/Retina, Bildaustausch im Dark Mode |
button | CTA mit kugelsicherem VML-Rendering für Outlook, konfigurierbare Breite (px/%) |
video | Thumbnail mit SVG-Play-Button-Overlay, verlinkt auf die Video-URL |
rawHtml | Einfügen von rohem HTML (mit Bedacht verwenden) |
Layoutblöcke
| Typ | Beschreibung |
|---|---|
columns | Layouts mit 1–4 Spalten, Verhältnis-Presets, Styling je Spalte, Abstand, kein Stapeln, umgekehrte Reihenfolge auf Mobilgeräten |
container | Verschachtelte Blockgruppierung mit Hintergrund, Rahmen, Border-Radius |
hero | Abschnitt mit Hintergrundbild samt VML-Fallback für Outlook, Overlay, vertikaler Ausrichtung |
divider | Horizontale Linie mit Farbe, Stärke, Breite, Stil |
spacer | Vertikaler Abstand |
Interaktive Blöcke
| Typ | Beschreibung |
|---|---|
accordion | Ausklappbare Abschnitte, nur mit CSS (interaktiv in Apple Mail/iOS, ~40 % der Clients; anderswo ausgeklappter Fallback) |
menu | Horizontale Navigation mit reinem CSS-Hamburger-Umschalter auf Mobilgeräten |
carousel | Bilder-Slideshow, nur mit CSS, mit Navigationspunkten (interaktiv in Apple Mail/iOS, sonst wird das erste Bild gezeigt) |
Datenblöcke
| Typ | Beschreibung |
|---|---|
table | Datentabelle mit Rich-Zellen, Spaltenbreiten, colSpan/rowSpan, Kopf, Fuß, Beschriftung, gestreiften Zeilen |
social | Icon-Links zu sozialen Netzwerken (gefüllt/Outline) mit Text-Fallback aus Initialen |
list | Tabellenbasierte Liste (Aufzählung, nummeriert, Häkchen, Icon) — vermeidet Client-Inkonsistenzen bei <ul>/<ol> |
progressBar | Visueller Fortschrittsbalken mit Label, vollständig tabellenbasiert |
CSS-Inlining
Gmail, Yahoo und mehrere weitere Clients entfernen <style>-Tags aus E-Mails. Die CSS-Inlining-Engine schreibt die berechneten Styles direkt als Inline-style-Attribute auf die HTML-Elemente.
// Enabled by default
const html = renderEmailHtml(blocks);
// Disable for debugging
const html = renderEmailHtml(blocks, { inlineCss: false });
Was inline geschrieben wird: Basis-Styles (font-size, color, background-color, padding usw.) aus dem generierten <style>-Block werden Elementen zugeordnet und inline angewendet.
Was in <style> bleibt: Media Queries (responsiv), @media (prefers-color-scheme:dark) (Dark Mode), @keyframes (Animationen) und Regeln mit Pseudoselektoren. Diese benötigen den <style>-Block und degradieren in Clients, die ihn entfernen, sauber.
Der Inliner wird auch eigenständig exportiert:
import { inlineCss } from '@owlat/email-renderer';
const inlined = inlineCss('<style>.foo{color:red}</style><div class="foo">Hi</div>');
// <div class="foo" style="color:red">Hi</div>
Dark Mode
Der Renderer erzeugt vollständige Dark-Mode-Unterstützung über CSS-Regeln mit @media (prefers-color-scheme: dark).
Funktionen
- Meta-Tags (
color-scheme,supported-color-schemes) zur Erkennung durch den Client - Automatische Umkehrung der Textfarben (dunkle Hintergründe, helle Schrift)
- Anpassung der Linkfarbe (
#93c5fdim Dark Mode) - Reduzierte Bilddeckkraft (
opacity: 0.9) - Bildaustausch im Dark Mode: Setzen Sie
darkSrcan Bildblöcken, um im Dark Mode ein anderes Bild anzuzeigen
const imageBlock: EditorBlock = {
id: 'img-1',
type: 'image',
content: {
src: '/logo-light.png',
darkSrc: '/logo-dark.png', // Shown in dark mode
alt: 'Logo',
width: 100,
},
};
- Dark-Overrides je Block: Blöcke können
darkOverridesmit eigenen Werten fürbackgroundColorundtextColorangeben, die über CSS Custom Properties angewendet werden
Client-Unterstützung
| Client | Dark-Mode-Unterstützung |
|---|---|
| Apple Mail / iOS | Vollständig (Media Query) |
| Outlook.com | Vollständig (Media Query) |
| Gmail (mobil) | Teilweise (erzwingt eigenen Dark Mode) |
| Outlook Desktop | Keine |
| Gmail (Web) | Keine |
VML-Unterstützung für Outlook
Outlook Desktop ignoriert CSS-border-radius, background-image auf Divs und viele moderne CSS-Eigenschaften. Der Renderer erzeugt VML-Fallbacks (Vector Markup Language), eingebettet in bedingte MSO-Kommentare.
Kugelsichere Buttons
Buttons werden mit v:roundrect-VML-Elementen gerendert, die abgerundete Ecken und Hintergrundfarben in Outlook unterstützen:
<!--[if mso]>
<v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" href="https://..."
style="height:44px;v-text-anchor:middle;width:200px"
arcsize="18%" fillcolor="#4f46e5" stroke="f">
<v:textbox inset="0,0,0,0">
<center style="color:#ffffff;font-size:16px">Click here</center>
</v:textbox>
</v:roundrect>
<![endif]-->
Hintergrundbilder
Hero-Blöcke und Container mit Hintergrundbildern nutzen v:image-VML für die Outlook-Kompatibilität.
Tabellen mit fester Breite
Das Boilerplate umschließt den Inhalt mit MSO-bedingten Tabellen fester Breite, um baseWidth in Outlook durchzusetzen (das max-width ignoriert).
Responsives Design
Stapeln auf Mobilgeräten
Spaltenlayouts stapeln sich auf Mobilgeräten automatisch (unterhalb des konfigurierten breakpoint). Das lässt sich je Block steuern:
// Entire columns block
content.mobileStacking = true; // default: true
// Per-column override
content.columnStyles = [
{ stackOnMobile: false }, // This column won't stack
{}, // This column follows parent setting
];
// Reverse stack order on mobile
content.mobileStackOrder = 'reverse'; // Image-right becomes image-first on mobile
Responsive Sichtbarkeit
Jeder Block unterstützt die Flags hideOnMobile und hideOnDesktop für responsives Ein- und Ausblenden:
content.hideOnMobile = true; // Hidden below breakpoint
content.hideOnDesktop = true; // Hidden above breakpoint
Responsive Schriftgröße
Textblöcke unterstützen eine separate mobileFontSize, die über eine Media Query angewendet wird:
const textBlock = {
type: 'text',
content: {
fontSize: 18, // Desktop
mobileFontSize: 14, // Mobile
},
};
Fluide Bilder
Bilder werden auf Mobilgeräten über die CSS-Klasse owlat-fluid-img automatisch fluide (width: 100%; height: auto).
Bedingte Inhalte
Blöcke lassen sich zur Renderzeit anhand von Variablenwerten ein- oder ausblenden:
const block: EditorBlock = {
type: 'text',
content: {
html: '<p>Premium member content</p>',
condition: {
variable: 'plan',
operator: 'equals',
value: 'premium',
},
},
};
// Only renders if plan === 'premium'
renderEmailHtml([block], {
variableValues: { plan: 'premium' },
});
Unterstützte Operatoren: exists, notExists, equals, notEquals, contains.
Wiederholungsblöcke
Über Array-Variablen iterieren und einen Block einmal je Element rendern — nützlich für Produktlisten im E-Commerce, Bestellpositionen und Empfehlungszeilen:
const block: EditorBlock = {
type: 'text',
content: {
html: '<p>{{product.name}} — {{product.price}}</p>',
repeat: {
variable: 'products', // Key in variableValues (JSON-encoded array)
itemAlias: 'product', // Alias for {{product.field}} interpolation
maxItems: 5, // Optional cap on iterations
},
},
};
renderEmailHtml([block], {
variableValues: {
products: JSON.stringify([
{ name: 'Widget', price: '$9.99' },
{ name: 'Gadget', price: '$19.99' },
]),
},
});
Innerhalb wiederholter Blöcke referenzieren Sie Elementeigenschaften mit und den nullbasierten Iterationsindex mit . Ist maxItems gesetzt, werden nur die ersten N Elemente gerendert.
Wiederholungsblöcke lassen sich mit bedingten Inhalten kombinieren — die Bedingung wird einmal für den Block ausgewertet, während die Wiederholung über das Array iteriert.
Verlaufshintergründe
Buttons, Container und Hero-Blöcke unterstützen Verlaufshintergründe über das Interface GradientBackground:
interface GradientBackground {
direction: string; // CSS direction, e.g. 'to right', '135deg'
stops: Array<{ color: string; position: number }>;
}
// Example: gradient button
const buttonBlock: EditorBlock = {
type: 'button',
content: {
text: 'Get Started',
url: 'https://example.com',
backgroundColor: '#4f46e5', // Solid fallback
backgroundGradient: {
direction: 'to right',
stops: [
{ color: '#4f46e5', position: 0 },
{ color: '#7c3aed', position: 100 },
],
},
// ...other button props
},
};
Der Renderer gibt ein CSS-linear-gradient mit der einfarbigen backgroundColor als Fallback aus. Für Outlook sorgt ein VML-Element <v:fill type="gradient"> für die Verlaufsdarstellung, wo CSS-Verläufe nicht unterstützt werden.
Link-Transformation
Wenden Sie UTM-Parameter, Klick-Tracking oder beliebiges URL-Rewriting auf alle Links in der E-Mail an:
const html = renderEmailHtml(blocks, {
linkTransform: (url, { blockType, blockId }) => {
const u = new URL(url);
u.searchParams.set('utm_source', 'email');
u.searchParams.set('utm_medium', blockType);
return u.toString();
},
});
Die Transformation wird auf Links in Buttons, Bildern, Social-Icons, Menüpunkten und Karussells angewendet.
CSS-Animationen
Animationen als Progressive Enhancement, die nur abgespielt werden, wenn die Nutzerin oder der Nutzer Bewegung zulässt:
/* Generated automatically */
@media (prefers-reduced-motion: no-preference) {
.owlat-animate-fade-in { animation: owlat-fade-in 0.6s ease-out both; }
.owlat-animate-slide-up { animation: owlat-slide-up 0.6s ease-out both; }
}
Anwendung an Blöcken über cssClass:
content.cssClass = 'owlat-animate-fade-in';
Funktioniert in Apple Mail und iOS Mail. Anderswo wird es stillschweigend ignoriert.
Klartext-Renderer
Multipart-Klartext für Barrierefreiheit und Zustellbarkeit erzeugen:
import { renderPlainText } from '@owlat/email-renderer';
const text = renderPlainText(blocks, { baseWidth: 600 });
Alle Blocktypen haben eine Klartextdarstellung:
| Block | Klartextausgabe |
|---|---|
text | HTML entfernt, Links extrahiert |
button | [Button Text] (URL) |
image | [Image: alt text] oder [Image: alt text] (link) |
divider | Trenner --- |
table | Zeilen mit Pipe-Trennung, samt Kopf/Fuß |
social | Plattformname + URL je Zeile |
list | - item / 1. item / [x] item |
progressBar | [Progress: 75%] |
carousel | [Image 1: alt] (link) je Bild |
Block-Validator
Die strukturelle Validierung vor dem Rendern findet Probleme, bevor gerendert wird:
import { validateBlocks } from '@owlat/email-renderer';
const { valid, issues } = validateBlocks(blocks);
for (const issue of issues) {
console.log(`[${issue.severity}] ${issue.code}: ${issue.message}`);
}
Validierungsprüfungen
- Text: Erkennung leerer Inhalte
- Bild: fehlendes
src, fehlenderalt-Text, ungültige Abmessungen - Button: leerer Text, leere URL, Platzhalter-URL (
#) - Video: fehlende Video-URL, fehlendes Thumbnail
- Spalten: leere Spalten, zu tiefe Verschachtelung
- Tabelle: leere Kopfzeilen, leere Zeilen
- Karussell: keine Bilder, Bilder ohne
srcoderalt - Liste: leere Einträge, Icon-Typ ohne Icon-URL
- ProgressBar: Wert außerhalb des Bereichs 0–100, zu geringer Kontrast zwischen Balken- und Schienenfarbe
- Allgemein: zu hohe Blockanzahl, tief verschachtelte Strukturen
Barrierefreiheits-Audit
Aktivieren Sie das Barrierefreiheits-Audit für WCAG-orientierte Prüfungen:
const { issues } = validateBlocks(blocks, { accessibilityAudit: true });
Zusätzliche Prüfungen umfassen:
- Farbkontrast: Kontrastverhältnis zwischen Buttontext und -hintergrund unter 4,5:1 (WCAG AA)
- Überschriftenhierarchie: übersprungene Überschriftenebenen (z. B. h1 gefolgt von h3)
- Qualität von Linktexten: erkennt vage Linktexte wie „click here“ oder „read more“
- Tabellenbeschriftungen: Tabellen ohne
captionTextfür Screenreader - Alt-Texte von Bildern: Karussellbilder ohne beschreibenden Alt-Text
E-Mail-Analyse
Analyse nach dem Rendern für Zustellbarkeit und Qualität:
import { analyzeEmail } from '@owlat/email-renderer';
const analysis = analyzeEmail(html, { subjectLine: 'Your order' });
Metriken
| Metrik | Beschreibung |
|---|---|
htmlSizeBytes | Gesamtgröße des HTML |
exceedsGmailClip | Ob die E-Mail Gmails Clipping-Schwelle von 102 KB überschreitet |
tableNestingDepth | Maximale Tabellenverschachtelung (>10 kann zu Renderproblemen führen) |
imageCount | Gesamtzahl der Bilder (>20 löst eine Warnung aus) |
linkCount | Gesamtzahl der Links (>60 löst eine Spamfilter-Warnung aus) |
hasTextContent | Ob die E-Mail nennenswerten Text enthält (nicht nur Bilder) |
textToImageRatio | Zeichen je Bild (höher ist besser für die Zustellbarkeit) |
displayNoneCount | Verborgene Elemente über den Preheader hinaus (übermäßig = Spamsignal) |
styleBlockSizeBytes | Summierte Größe des Inhalts aller <style>-Blöcke |
exceedsGmailCssLimit | Ob der <style>-Block Gmails Grenze von ~8 KB überschreitet (Gmail entfernt dann den gesamten Block, wodurch responsives Layout und Dark Mode brechen) |
cssValidationIssues | Grundlegende CSS-Syntaxprobleme (unausgeglichene Klammern, nicht geschlossene Strings); entfällt, wenn keine vorliegen |
warnings | Array mit umsetzbaren Empfehlungen |
Übergeben Sie eine subjectLine, damit zusätzlich Betreffzeilen über 70 Zeichen (Gmail-Kürzung)
und Betreffzeilen in GROSSBUCHSTABEN (Spamsignal) markiert werden. Das Array warnings weist
darüber hinaus auf HTML hin, das in gängigen Clients bricht: position:absolute (von Gmail
entfernt), <form>/<input>-Elemente (außerhalb von Apple Mail/iOS entfernt) und
<video>/<audio>-Tags (nur Apple Mail/iOS).
Übergeben Sie { includeBreakdown: true }, um im Ergebnis sizeBreakdown (Größe je
Inhaltskategorie) und optimizations (Vorschläge mit geschätzter Byte-Ersparnis) zu
befüllen:
const analysis = analyzeEmail(html, { subjectLine: 'Your order', includeBreakdown: true });
Kompatibilitätsdaten der Clients
Die Feature- und Property-Kompatibilität je Block liegt in dem jeweiligen
Block-Modul (unter packages/email-renderer/src/blocks/<type>/) und wird über den
Compatibility-Walker bereitgestellt, den @owlat/email-renderer exportiert.
Das Paket @owlat/shared besitzt weiterhin die übergreifenden Typen, die
Client-Metadaten und die Erweiterungs-Registries für Plugins.
import {
getBlockCompatibility,
getPropertyCompatibility,
getCriticalProperties,
getClientPropertyIssues,
} from '@owlat/email-renderer';
import { emailClients, registerBlockCompatibility } from '@owlat/shared';
Kompatibilität je Block
const compat = getBlockCompatibility('accordion');
// [{
// feature: 'CSS-only toggle',
// description: 'Expandable/collapsible sections using :checked selector',
// support: { gmail: 'none', outlookDesktop: 'none', appleMail: 'full', ... },
// fallback: 'Falls back to all sections expanded in unsupported clients — content always visible',
// owlatHandled: true,
// canIEmailSlug: 'css-pseudo-class-checked',
// }, ...]
Kompatibilität je Eigenschaft
const props = getPropertyCompatibility('image', 'borderRadius');
// [{
// property: 'borderRadius',
// description: 'Rounded corners on images',
// support: { gmail: 'full', outlookDesktop: 'none', appleMail: 'full', ... },
// severity: 'warning',
// recommendation: 'Use PNG with baked-in corners for Outlook',
// owlatHandled: false,
// }]
Schweregrade
- critical — Funktion in großen Clients vollständig kaputt, kein Fallback
- warning — in manchen Clients eingeschränkt, Fallback vorhanden
- info — geringfügige visuelle Unterschiede, unbedenklich
Unterstützte Clients
Gmail (Web), Gmail (mobile App), Outlook Desktop (Classic), Outlook 365 (Web), Outlook (New), Outlook (Mac), Apple Mail, iOS Mail, Yahoo Mail, Samsung Mail, Thunderbird, ProtonMail.
AMP for Email
Erzeugen Sie AMP4Email-kompatibles HTML als drittes Format neben HTML und Klartext. AMP-E-Mails unterstützen interaktive Komponenten (Accordion, Karussell) in den entsprechenden Clients nativ.
import { renderAmpEmail } from '@owlat/email-renderer';
const amp = renderAmpEmail(blocks, {
title: 'Your weekly update',
baseWidth: 600,
lang: 'en',
});
Der AMP-Renderer ersetzt die rein CSS-basierten interaktiven Komponenten (Accordion, Karussell) durch ihre AMP-Entsprechungen (amp-accordion, amp-carousel). Layoutblöcke (columns, hero, container) rekursieren in ihre Kinder; tabellenbasierte Blöcke (table, list, progress bar, menu) verwenden ihr AMP-gültiges Markup weiter; und bildhaltige Blöcke (image, social, video) geben <amp-img> aus. Der einzige Block ohne AMP-Entsprechung ist der Raw-HTML-Block — AMP verbietet beliebiges HTML, daher entfällt er in der AMP-Ausgabe.
Zustellung: Wenn Sie eine blockbasiert gestaltete Postbox-E-Mail senden, die einen interaktiven Block (Accordion/Karussell) verwendet, wird das AMP-Rendering automatisch als alternativer Teil text/x-amp-html neben den HTML- und Klartextteilen angehängt. AMP-fähige Clients zeigen die interaktive Fassung, alle anderen fallen auf das HTML zurück. (Designs ohne interaktiven Block werden nur mit HTML + Text ausgeliefert — die AMP-Variante wäre Byte für Byte gleichwertig.)
Client-Unterstützung: Gmail (Web & mobil), Yahoo Mail, Mail.ru, FairEmail. Die meisten anderen Clients zeigen den HTML-Fallback aus der Multipart-Nachricht.
Template-Diff
Vergleichen Sie zwei gerenderte HTML-Strings und erkennen Sie strukturelle Änderungen. Nützlich für die Versionierung von Templates, die Überprüfung von A/B-Tests und die Regressionserkennung.
import { diffEmails } from '@owlat/email-renderer';
const diff = diffEmails(oldHtml, newHtml);
if (!diff.identical) {
console.log(`${diff.changes.length} changes detected (${diff.sizeDelta > 0 ? '+' : ''}${diff.sizeDelta} bytes)`);
for (const change of diff.changes) {
console.log(`[${change.type}] ${change.category}: ${change.description}`);
}
}
Jede Änderung enthält einen type (added, removed, modified), eine category (text, style, image, link, structure, meta) und zusammenfassende stats mit Zählern je Kategorie.
Block-Module
Jeder eingebaute Blocktyp ist ein Block-Modul — eine einzelne Vertikale, die
HTML, Klartext, AMP, Validierung, die Factory für Standardinhalte, Platzierungs-
Metadaten und Kompatibilitätsdaten dieses Typs besitzt. Module registrieren sich beim
Laden des Pakets selbst (siehe packages/email-renderer/src/blocks/_builtin-modules.ts),
und der Walker in packages/email-renderer/src/blocks/index.ts verteilt nach
block.type. Jedes Modul liegt in einem eigenen Verzeichnis, z. B. exportiert
packages/email-renderer/src/blocks/text/index.ts das textModule.
Das ist die moderne Erweiterungs-API. Um einen eigenen Blocktyp auszuliefern, implementieren
Sie BlockModule<T> (definiert in packages/email-renderer/src/blocks/_module.ts)
und registrieren ihn mit registerBlockModule(), bevor die Registry eingefroren wird:
import {
registerBlockModule,
finalizeBlockRegistry,
} from '@owlat/email-renderer';
import type { BlockModule } from '@owlat/email-renderer';
const ratingModule: BlockModule<'rating'> = {
type: 'rating',
// Inner HTML only — the Walker wraps it for the placement.
html({ content, ctx }) {
const { value, maxValue } = content as { value: number; maxValue: number };
const stars = '★'.repeat(value) + '☆'.repeat(maxValue - value);
return `<tr><td align="center" style="font-size:24px;padding:${ctx.theme.spacingUnit ?? 8}px 0">${stars}</td></tr>`;
},
// Optional: plaintext, amp, validate, isEmpty, layout, applyTheme,
// responsiveCss, compatibility, createDefault, placements.
};
registerBlockModule(ratingModule);
finalizeBlockRegistry(); // prevents further registration
Oberfläche eines Moduls
Ein BlockModule<T> ist generisch über seinen Blocktyp, sodass content auf
diese Variante eingegrenzt wird. Nur type und html sind erforderlich, alles andere ist optional.
| Member | Zweck |
|---|---|
type | Diskriminante des Blocktyps (erforderlich) |
html(args) | Inneres HTML des Blocks (erforderlich); das Umschließen für die Platzierung übernimmt der Walker |
placements | Platzierungen, die der Block zulässt: root, column, container, hero (Standard ['root']) |
plaintext(args) | Klartextdarstellung für Multipart-E-Mails |
amp(args) | AMP4Email-Variante; ein Block wird aus der AMP-Ausgabe ausgeschlossen, indem undefined zurückgegeben wird |
validate(args) | Fachliche Validierung; wird automatisch in validateBlocks() eingebunden |
preflight(args) | Nicht fatale Warnungen zur Renderzeit |
isEmpty(content) | Lässt den Walker leere Blöcke vor dem Umschließen überspringen |
layout(content) | Overrides für das Abschnittslayout (background, padding, sectionMode) |
applyTheme(content, theme) | Wendet vom Theme abgeleitete Standardwerte an; Werte auf Blockebene gewinnen immer |
responsiveCss(args) | Gibt responsives CSS je Block aus (z. B. mobile Schriftgröße) |
compatibility | { features, properties }, gelesen vom Compatibility-Walker |
createDefault(theme) | Standardinhalt für die „Block hinzufügen“-Aktion des Editors |
Zusammengesetzte Blöcke (columns, container, hero) rekursieren über den in args
übergebenen Rekursionseinstieg walk in ihre Kinder — sie rufen nie direkt andere
Module auf. Die Helfer der Registry sind registerBlockModule,
unregisterBlockModule, finalizeBlockRegistry, isBlockRegistryFrozen,
moduleFor(type) und registeredBlockTypes().
Registry für eigene Blöcke (Legacy)
Der Weg über registerBlock(type, renderer) weiter unten ist der Legacy-Fallback für
eigene Renderer, der aus Gründen der Abwärtskompatibilität erhalten bleibt. Neuer Code sollte
stattdessen ein BlockModule implementieren und registerBlockModule() (siehe oben) aufrufen —
ein Modul besitzt HTML, Klartext, AMP, Validierung und Kompatibilität in einer Einheit, während ein
BlockRenderer nur HTML erzeugt.
Registrieren Sie eigene Block-Renderer für Blocktypen von Drittanbietern oder für anwendungsspezifische Typen. Eigene Blöcke werden neben den eingebauten Blöcken gerendert, können diese aber nicht überschreiben.
Einen Block-Renderer definieren
Eine BlockRenderer-Funktion erhält den Blockinhalt, den Render-Kontext und das vollständige Blockobjekt und liefert einen HTML-String zurück:
import type { BlockRenderer, RenderContext } from '@owlat/email-renderer';
import type { EditorBlock } from '@owlat/shared';
const renderRating: BlockRenderer = (content, ctx, block) => {
const { value, maxValue } = content as { value: number; maxValue: number };
const stars = '★'.repeat(value) + '☆'.repeat(maxValue - value);
return `<tr><td align="center" style="font-size:24px;padding:${ctx.theme.spacingUnit ?? 8}px 0">${stars}</td></tr>`;
};
Registrieren und Einfrieren
import { registerBlock, finalizeRegistry } from '@owlat/email-renderer';
// Register during application setup
registerBlock('rating', renderRating);
// Freeze the registry to prevent runtime mutation
finalizeRegistry();
Nach dem Aufruf von finalizeRegistry() werfen weitere Aufrufe von registerBlock() oder unregisterBlock() einen Fehler. Mit isRegistryFinalized() prüfen Sie den Einfrierzustand.
Eigene Blöcke verwenden
Einmal registriert, rendern eigene Blöcke wie jeder eingebaute Block:
const blocks: EditorBlock[] = [
{
id: 'block-1',
type: 'rating' as any,
content: { value: 4, maxValue: 5 },
},
];
const html = renderEmailHtml(blocks);
E-Mail-Gesundheitswert
Berechnen Sie einen Gesamt-Gesundheitswert (0–100) über Kompatibilität, Barrierefreiheit, Zustellbarkeit und die Outlook-spezifische Unterstützung hinweg:
import { getEmailHealthScore, renderEmailHtml } from '@owlat/email-renderer';
const html = renderEmailHtml(blocks);
const health = getEmailHealthScore(blocks, html);
console.log(`Score: ${health.overall}/100`);
console.log(`Compatibility: ${health.compatibility}`);
console.log(`Accessibility: ${health.accessibility}`);
console.log(`Deliverability: ${health.deliverability}`);
console.log(`Outlook support: ${health.outlookSupport}`);
for (const rec of health.recommendations) {
console.log(`[${rec.impact}] ${rec.category}: ${rec.message}`);
}
overall ist eine gewichtete Zusammenfassung der vier Teilwerte. Jede Empfehlung
trägt eine category aus compatibility, accessibility, deliverability oder
outlook sowie ein impact aus high, medium oder low.
Client-Simulatoren
Die Render-Option targetClient (siehe Render-Optionen)
degradiert die Ausgabe bereits im Renderaufruf. Dieselben Degradierungs-Transformationen
sind auch eigenständig verfügbar, sodass Sie jeden HTML-String nachbearbeiten können —
etwa um eine Vorschaugalerie nebeneinander zu erzeugen, ohne neu zu rendern:
import { simulateClient } from '@owlat/email-renderer';
const gmailView = simulateClient(html, 'gmail'); // strips <style>, etc.
const outlookView = simulateClient(html, 'outlookDesktop');
Eingebaute Simulatoren gibt es für gmail, outlookDesktop, outlookNew,
appleMail und yahooMail (die Union TargetClient). Unbekannte Clients reichen
das HTML unverändert durch.
Die Registry ist erweiterbar. Installieren oder ersetzen Sie einen Simulator vor der Nutzung; die
Helfer liegen in packages/email-renderer/src/simulators/registry.ts:
import {
registerClientSimulator,
unregisterClientSimulator,
clientSimulators,
} from '@owlat/email-renderer';
import type { ClientSimulator } from '@owlat/email-renderer';
const myGmail: ClientSimulator = (html) => html.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '');
registerClientSimulator('gmail', myGmail); // replaces the built-in
Ein ClientSimulator ist eine Transformation (html: string) => string.
Optimierungsvorschläge
Erhalten Sie umsetzbare Vorschläge zur Reduzierung der E-Mail-Größe:
import { suggestOptimizations } from '@owlat/email-renderer';
const suggestions = suggestOptimizations(html);
for (const s of suggestions) {
console.log(`[${s.category}] ${s.description} (save ~${s.estimatedSavings} bytes)`);
}
Zu den Kategorien der Vorschläge zählen minification (Entfernen von Leerraum), vml (bedingte Outlook-Blöcke), css (Inlining des Style-Blocks) und images (nicht optimierte Bilder).
Exporte
// Rendering
export { renderEmailHtml, renderBlockFragment } from './renderer';
export { renderAmpEmail } from './amp';
export { renderPlainText } from './plaintext';
export { inlineCss } from './inliner';
// Analysis & validation
export { analyzeEmail, getEmailHealthScore, suggestOptimizations } from './analyzer';
export { validateBlocks, ValidationError } from './validator';
export { diffEmails } from './diff';
// Block module API (modern extension path)
export {
registerBlockModule, unregisterBlockModule, finalizeBlockRegistry,
isBlockRegistryFrozen, moduleFor, registeredBlockTypes,
} from './blocks/_registry';
export type {
BlockModule, BlockOf, ContentOf, Placement, RenderArgs,
PlainArgs, AmpArgs, ValidateArgs, HtmlWalk, PlaintextWalk,
} from './blocks/_module';
// Legacy custom block registry (HTML-only renderers)
export { registerBlock, unregisterBlock, getRegisteredBlocks, finalizeRegistry, isRegistryFinalized } from './blocks';
export type { BlockRenderer } from './blocks';
// Client simulators
export {
simulateClient, registerClientSimulator,
unregisterClientSimulator, clientSimulators,
} from './simulators';
export type { ClientSimulator } from './simulators';
// Compatibility (per-block data lives in Block modules, surfaced via the walker)
export {
getBlockCompatibility, getPropertyCompatibility, getCriticalProperties,
getHandledFeatures, getClientIssues, getClientPropertyIssues, getAudienceReach,
scoreBlockCompatibility, getBlockLimitationSummary, getSafeBlockConfig,
checkPropertyCompatibility, featuresFor, propertiesFor,
allBlockTypes, allFeatures, allProperties,
} from './compatibility';
export type { BlockTaggedFeature, BlockTaggedProperty, BlockLimitation } from './compatibility';
// Sanitization utilities
export { escapeHtml, escapeAttr, sanitizeUrl, sanitizeCss, sanitizeRawHtml } from './sanitize';
// Types
export type { RenderOptions, RenderContext, TargetClient, ValidationLevel, EmailHealthScore, EmailHealthRecommendation, GmailAnnotations } from './types';
export type { EmailAnalysis, EmailSizeBreakdown, OptimizationSuggestion } from './analyzer';
export type { ValidationIssue, ValidateOptions } from './validator';
export type { EmailDiff, EmailDiffChange } from './diff';
Die Erweiterungspunkte für Plugins rund um Kompatibilitätsmetadaten — registerEmailClient,
registerBlockCompatibility und die Registry emailClients — leben in
@owlat/shared, nicht in diesem Paket.
Dateistruktur
packages/email-renderer/src/
├── index.ts # Public package surface (see Exports)
├── renderer.ts # Main entry: renderEmailHtml, renderBlockFragment
├── boilerplate.ts # HTML document wrapper (DOCTYPE, head, MSO tables)
├── styles.ts # CSS generation (resets, media queries, dark mode)
├── inliner.ts # CSS inlining engine
├── outlook.ts # VML helpers (roundrect, background images)
├── plaintext.ts # Plain text renderer
├── amp.ts # AMP for Email renderer
├── diff.ts # Template diff/change detection
├── analyzer.ts # Post-render email analysis
├── validator.ts # validateBlocks() orchestrator
├── sanitize.ts # HTML/CSS/URL sanitization utilities
├── types.ts # RenderOptions, RenderContext, EmailHealthScore, ...
├── helpers/
│ ├── index.ts
│ ├── table.ts # Table column width helpers
│ ├── gradient.ts # Gradient CSS/VML generation
│ ├── dimensions.ts # Width/height calculation helpers
│ ├── padding.ts # Padding shorthand helpers
│ ├── inline-styles.ts # Inline style string builders
│ ├── linkTransform.ts # Link URL rewriting
│ ├── text.ts # Text/HTML helpers
│ └── validation.ts # Shared validation helpers
├── validators/
│ ├── index.ts
│ └── registry.ts # Block validator registry (bridges module.validate)
├── compatibility/
│ ├── index.ts # getBlockCompatibility, scoreBlockCompatibility, ...
│ ├── scoring.ts # Compatibility/audience-reach scoring
│ ├── ui.ts # Builder-UI limitation summaries & fix suggestions
│ └── walker.ts # Dispatches over registered Block modules
├── simulators/
│ ├── index.ts # simulateClient + registry re-exports
│ ├── registry.ts # registerClientSimulator / clientSimulators
│ ├── gmail.ts
│ ├── outlookDesktop.ts
│ ├── outlookNew.ts
│ ├── appleMail.ts
│ └── yahooMail.ts
├── preview/
│ ├── fixtures.ts # Sample blocks for preview generation
│ ├── generate.ts
│ └── assets/
└── blocks/
├── index.ts # Walker: dispatches by block.type, owns placement wrapping
├── _module.ts # BlockModule<T> interface + arg/walk types
├── _registry.ts # registerBlockModule, moduleFor, finalizeBlockRegistry
├── _builtin-modules.ts # Side-effect registration of all 17 built-in modules
├── text/index.ts # exports textModule
├── image/index.ts # exports imageModule (srcset, dark swap)
├── button/index.ts # exports buttonModule (VML bulletproof)
├── divider/index.ts
├── spacer/index.ts
├── columns/index.ts # stacking, reverse, gap
├── social/index.ts
├── container/index.ts # nesting, VML bg
├── hero/index.ts # VML bg image
├── table/index.ts # rich cells, colSpan/rowSpan
├── accordion/index.ts # CSS-only toggle
├── menu/index.ts # hamburger
├── carousel/index.ts # CSS-only radio nav
├── list/index.ts # table-based
├── progressBar/index.ts
├── rawHtml/index.ts
└── video/index.ts
Jedes Blockverzeichnis enthält eine index.ts, die ein <type>Module exportiert
(z. B. exportiert text/index.ts das textModule). Sie werden beim Laden des Pakets
gesammelt von _builtin-modules.ts registriert.