eQuantic.UIeQuantic.UI
Docs
Playground
GitHub
DocsWrite-once
E-Mail-Rendering (Track M)
Edit this page
6 min read
Seit 0.2.0-preview.45
STATUS: DER KERN WURDE IN 0.2.0-PREVIEW.45 AUSGELIEFERT (gemergt am 2026-08-25). EmailRealizer und EmailRenderer existieren in eQuantic.UI.Email mit dem Vokabular unten. Noch vor uns: die Matrix echter Clients (ein erzeugtes Beispiel gegen Gmail, Outlook und Apple Mail) und die HTML-Pins, sobald die Ausgabe sich gesetzt hat.
Schreiben Sie die E-Mail in C# mit denselben Komponenten, mit denen eine Seite geschrieben wird, und bekommen Sie HTML zurück, das ein E-Mail-Client tatsächlich rendern wird.
Warum das zur Architektur passt, statt gegen sie zu kämpfen
Eine Komponente ist hier ein VisualNode-Baum, der nichts von seinem Ziel weiß. Der Web-Realisierer macht daraus DOM und CSS; Photon macht daraus GPU-Malen. E-Mail ist ein dritter Realisierer: ein Ziel, dessen Render-Engine eben sehr restriktiv ist, was architektonisch dieselbe Form hat wie ein Ziel ganz ohne DOM.
An der Schreibschicht ändert sich nichts. Dieselben Column, Text, Image und Theme-Tokens, dasselbe Build(context).
Was schon existiert
Zwei Teile liegen heute im Baum, durch Lesen verifiziert:
•
WebRealizer.Lower(node, theme, typeScale, styles: null): Ohne Style-Senke bleiben die Styles inline am Element, statt zu deduplizierten atomaren Klassen zu werden. Inline-Style ist die härteste Beschränkung der E-Mail, und dieser Weg erzeugt ihn schon.
•
HtmlRenderer.RenderNode(...) macht aus dem abgesenkten Baum einen HTML-String.
Also sind "C#-Komponente → HTML mit Inline-Styles" zwei Aufrufe, die es schon gibt. Was fehlt, ist alles, was mit dem Medium zu tun hat.
Was E-Mail wirklich verlangt
Verlangen
Warum
Worin der Web-Realisierer abweicht
Layout aus geschachtelten <table>
Outlook unter Windows rendert mit der Engine von Word: kein Flexbox, kein Grid
Row/Column senken sich zu FlexDirection ab
Nur style="" inline
<style>-Blöcke und externes CSS werden von mehreren Clients entfernt oder ignoriert
Atomare Klassen + ein Stylesheet (der Weg mit Senke)
Wörtliche Farben
CSS-Custom-Properties werden nicht aufgelöst
Tokens geben var(--eq-color-…, fallback) aus
Absolute Bild-URLs oder CID
Gmail verwirft data:-URIs
Assets setzen eine ausgelieferte Origin voraus
Eine beschränkte Breite
~600px ist die Konvention, die jeder Client duldet
Seiten sind fließend
Keine Interaktion
Kein JS, bei den meisten Clients kein Hover, keine scrollenden Container
Pressable, ScrollView und Hover-Zustände existieren
Entwurf
Ein neues Paket, eQuantic.UI.Email, das nur von Primitives und Core abhängt, nie von eQuantic.UI.Web, damit die beiden Realisierer nicht ineinander abdriften können.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Genau wie eine Seite geschrieben.
public sealed class WelcomeEmail(string name) : StatelessComponent
{
public override VisualNode Build(ComponentContext context) =>
Column(gap: Space.S4, children: [
Image(Assets.Logo, width: 132),
Text($"Welcome, {name}", TypeRole.Heading),
Button("Confirm your address", href: "https://…"),
]);
}
// Dort gerendert, wo sie gesendet wird.
EmailMessage message = EmailRenderer.Render(new WelcomeEmail("Edgar"), theme);
// message.Html zu Tabellen abgesenkt, vollständig inline
// message.PlainText aus DEMSELBEN Baum erzeugt, nicht zweimal geschrieben
EmailMessage trägt beide Teile, weil eine Multipart-Nachricht beide braucht, und die Text-Alternative aus dem Baum zu erzeugen ist die einzige Art, wie sie mit dem HTML im Takt bleibt.
Scheiben
M0: messen, bevor gebaut wird. Fertig, und sie hat den Plan geändert. Neben den zwei erwarteten Befunden (das Flex-Layout, die light-dark()-Farben) ein dritter, den der Plan nicht hatte: Die Typografie lebt in eq-type-*-Klassen, und eine E-Mail hat kein Stylesheet, also rendete jeder Text in einer Größe. M2 wuchs, um die Rampe des Themes an jeden Text inline zu schreiben. Die verbleibende Hälfte von M0 ist menschlich: das erzeugte Beispiel gegen echte Postfächer.
M1: zu Tabellen absenken. Fertig. Box, Row, Column und Gap werden geschachtelte <table>/<tr>/<td> mit Zellen-Padding und Abstandszeilen. Die Ausrichtung bildet sich auf align/valign ab. Das ist der Hauptteil der Spur.
M2: Inline-Styles und wörtliches Theming. Fertig (dazu die inline geschriebene Typo-Rampe). Nutzt den Weg ohne Senke wieder; löst jedes Token in eine wörtliche Farbe und Länge auf, damit nichts von Custom-Properties abhängt. Dichte und Typskala lösen sich einmal auf, beim Rendern.
M3: Dokumenthülle. Fertig. EmailDocument: Doctype, der <meta>-Satz, der 600px-Container, die unsichtbare Preheader-Zeile und das Dark-Mode-Meta, das Clients beachten.
M4: Zaun des Vokabulars. Fertig. Diagnosen, die benennen, was eine E-Mail nicht kann: ScrollView, Pressable, Draggable, das Styling von Hover und Fokus, position: absolute. Ein Link wird ein <a>; ein Button wird ein kugelsicheres <a>, als Button gestylt. Ein Image ohne absolute URL ist ein Fehler, kein kaputtes Bild im Postfach von jemandem.
M5: Alternative in reinem Text. Fertig. Denselben Baum für den text/plain-Teil abgehen.
Was die Durchsicht dem Kontrakt beibrachte
Die Scheibe ging durch fünf Runden Durchsicht (32 Befunde), und was überlebte, sind Regeln, die es wert sind, gesagt zu werden:
•
Eine Eigenschaft, die das Medium nicht ausdrücken kann, wird eingezäunt, nicht angenähert: Gradienten-Tinte und MaxLines werfen und benennen den Ausweg, denn ein Vollton an ihrer Stelle ist eine andere Tinte, die niemand gewählt hat, und mehr Text zu zeigen, als die Autorin begrenzt hat, ist eine Abweichung im Inhalt.
•
Eine Eigenschaft, die das Medium anders ausdrückt, wird geehrt: das Padding des Containers (eine Hülltabelle mit einer Zelle), Cross und das AlignSelf je Kind (Zellenausrichtung), Radien je Ecke, Ränder, Überschriftenelemente, das aria-label aus Link.Label.
•
MainAlign ist hier gehaltlos, dokumentiert statt eingezäunt: Eine E-Mail-Tabelle bemisst sich nach ihrem Inhalt, es gibt also keinen freien Raum zu verteilen, und somit rendert jeder Wert gleich, was treu ist.
•
Eine kaputte Komponente lässt das SENDEN scheitern. Komponenten entfalten sich über Build, nicht über das BuildContained des Webs: Eine beschreibende Box ist richtig auf einer lebenden Seite, die ein Entwickler ansieht, und falsch in einer Nachricht, die kurz davor ist, das Postfach einer Leserin als Inhalt getarnt zu erreichen. Seit 0.2.0-preview.55 lässt ein Zyklus es genauso scheitern: Der Gang teilt die Tiefenzählung der Naht über ComponentBoundary.Enter, und jenseits der Grenze wirft er eine InvalidOperationException, statt einzufassen. Vorher war eine Komponente, die sich selbst baute, eine StackOverflowException, die nichts fangen kann, und der sendende Prozess starb, ohne etwas zu melden.
•
Adressen werden geparst, nicht auf Präfixe geprüft; Attributwerte werden als Attribute kodiert; ein Link um verlinkte Runs weigert sich, Anker zu schachteln, und benennt die Entscheidung.
Zäune
•
Nie ein <style>-Block, auch dort nicht, wo ein Client ihn unterstützt. Eine Regel schlägt eine Matrix von Ausnahmen.
•
Keine Interaktivität. Eine E-Mail ist eine gedruckte Seite, die eben Links hat.
•
Keine clientspezifischen Tricks in den Komponenten. Was Outlook braucht, lebt im Realisierer, nie in etwas, das eine Autorin schreibt.
•
Anhänge und Zustellung gehören nicht uns. Die Ausgabe ist HTML plus Text; das Senden ist die Aufgabe der Anwendung (MailKit, SES, was sie schon nutzt).
Wie es getestet wird
Die beiden gleichen Netze, die der Rest des Frameworks nutzt:
•
HTML-Pins, mit Absicht neu erzeugt wie die transpilierten Fixtures, sodass eine Layout-Änderung als Diff sichtbar wird statt als Überraschung.
•
Eine Client-Matrix als Konformität, gegen echte Clients ausgeführt und festgehalten: Die Regel, dass der Browser der Schiedsrichter ist, gilt hier noch härter, weil E-Mail-Clients weiter von einer Spezifikation entfernt sind als Browser.