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:

OptionTypStandardBeschreibung
themeEmailThemesiehe untenDesign-Tokens (Farben, Schriften, Abstände)
darkModebooleanfalseRendering der Dark-Mode-Vorschau aktivieren
preheaderTextstring''Verborgener Vorschautext im Posteingang
titlestring''Dokumenttitel (Browser-Tab)
baseWidthnumber600Inhaltsbreite in px (typisch 500–700)
breakpointnumber480Responsiver Mobil-Breakpoint in px
direction'ltr' | 'rtl''ltr'Textrichtung für RTL-Sprachen
langstring'en'HTML-Attribut lang
inlineCssbooleantrueCSS für Gmail/Yahoo inline in die Elemente schreiben
minifybooleanfalseAusgabe-HTML minifizieren (MSO-Kommentare bleiben erhalten)
variableTypeVariableType'personalization'Rendermodus für Variablen
variableValuesRecord<string, string>{}Werte für die Auswertung bedingter Inhalte
fontUrlsstring[][]URLs von Webfonts, die importiert werden sollen
customCssstring''Eigenes CSS, das in den Style-Block eingefügt wird
linkTransformFunctionAlle Link-URLs transformieren (UTM, Klick-Tracking)
onWarningFunctionCallback für nicht fatale Render-Warnungen
targetClientTargetClientRendering für einen bestimmten E-Mail-Client simulieren (gmail, outlookDesktop, outlookNew, appleMail, yahooMail)
validationLevelValidationLevel'soft'Strenge der Validierung: skip (keine), soft (warnen), strict (Fehler werfen)
gmailAnnotationsGmailAnnotationsAnnotationen 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

TypBeschreibung
textRich Text (Absatz, h1, h2, h3) mit Schriftgröße, Farbe, Ausrichtung, Zeilenhöhe
imageResponsives Bild mit Alt-Text, Link, srcset/Retina, Bildaustausch im Dark Mode
buttonCTA mit kugelsicherem VML-Rendering für Outlook, konfigurierbare Breite (px/%)
videoThumbnail mit SVG-Play-Button-Overlay, verlinkt auf die Video-URL
rawHtmlEinfügen von rohem HTML (mit Bedacht verwenden)

Layoutblöcke

TypBeschreibung
columnsLayouts mit 1–4 Spalten, Verhältnis-Presets, Styling je Spalte, Abstand, kein Stapeln, umgekehrte Reihenfolge auf Mobilgeräten
containerVerschachtelte Blockgruppierung mit Hintergrund, Rahmen, Border-Radius
heroAbschnitt mit Hintergrundbild samt VML-Fallback für Outlook, Overlay, vertikaler Ausrichtung
dividerHorizontale Linie mit Farbe, Stärke, Breite, Stil
spacerVertikaler Abstand

Interaktive Blöcke

TypBeschreibung
accordionAusklappbare Abschnitte, nur mit CSS (interaktiv in Apple Mail/iOS, ~40 % der Clients; anderswo ausgeklappter Fallback)
menuHorizontale Navigation mit reinem CSS-Hamburger-Umschalter auf Mobilgeräten
carouselBilder-Slideshow, nur mit CSS, mit Navigationspunkten (interaktiv in Apple Mail/iOS, sonst wird das erste Bild gezeigt)

Datenblöcke

TypBeschreibung
tableDatentabelle mit Rich-Zellen, Spaltenbreiten, colSpan/rowSpan, Kopf, Fuß, Beschriftung, gestreiften Zeilen
socialIcon-Links zu sozialen Netzwerken (gefüllt/Outline) mit Text-Fallback aus Initialen
listTabellenbasierte Liste (Aufzählung, nummeriert, Häkchen, Icon) — vermeidet Client-Inkonsistenzen bei <ul>/<ol>
progressBarVisueller 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 (#93c5fd im Dark Mode)
  • Reduzierte Bilddeckkraft (opacity: 0.9)
  • Bildaustausch im Dark Mode: Setzen Sie darkSrc an 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 darkOverrides mit eigenen Werten für backgroundColor und textColor angeben, die über CSS Custom Properties angewendet werden

Client-Unterstützung

ClientDark-Mode-Unterstützung
Apple Mail / iOSVollständig (Media Query)
Outlook.comVollständig (Media Query)
Gmail (mobil)Teilweise (erzwingt eigenen Dark Mode)
Outlook DesktopKeine
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.

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:

BlockKlartextausgabe
textHTML entfernt, Links extrahiert
button[Button Text] (URL)
image[Image: alt text] oder [Image: alt text] (link)
dividerTrenner ---
tableZeilen mit Pipe-Trennung, samt Kopf/Fuß
socialPlattformname + 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, fehlender alt-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 src oder alt
  • 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 captionText fü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

MetrikBeschreibung
htmlSizeBytesGesamtgröße des HTML
exceedsGmailClipOb die E-Mail Gmails Clipping-Schwelle von 102 KB überschreitet
tableNestingDepthMaximale Tabellenverschachtelung (>10 kann zu Renderproblemen führen)
imageCountGesamtzahl der Bilder (>20 löst eine Warnung aus)
linkCountGesamtzahl der Links (>60 löst eine Spamfilter-Warnung aus)
hasTextContentOb die E-Mail nennenswerten Text enthält (nicht nur Bilder)
textToImageRatioZeichen je Bild (höher ist besser für die Zustellbarkeit)
displayNoneCountVerborgene Elemente über den Preheader hinaus (übermäßig = Spamsignal)
styleBlockSizeBytesSummierte Größe des Inhalts aller <style>-Blöcke
exceedsGmailCssLimitOb der <style>-Block Gmails Grenze von ~8 KB überschreitet (Gmail entfernt dann den gesamten Block, wodurch responsives Layout und Dark Mode brechen)
cssValidationIssuesGrundlegende CSS-Syntaxprobleme (unausgeglichene Klammern, nicht geschlossene Strings); entfällt, wenn keine vorliegen
warningsArray 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.

MemberZweck
typeDiskriminante des Blocktyps (erforderlich)
html(args)Inneres HTML des Blocks (erforderlich); das Umschließen für die Platzierung übernimmt der Walker
placementsPlatzierungen, 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)

Block-Module bevorzugen

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.