Skip to content

EmailRealizer

Edgar Mesquita edited this page Aug 24, 2026 · 2 revisions

Email Rendering (Track M)

🌐 This page in: English · Português

Status: planned, not built. Nothing below ships today. This page is the design and the slices; the roadmap entry points here.

Write the email in C# with the same components a page is written with, and get back HTML an email client will actually render.

Why this fits the architecture rather than fighting it

A component here is a VisualNode tree that knows nothing about its target. The web realizer turns it into DOM and CSS; Photon turns it into GPU paint. Email is a third realizer — a target whose rendering engine happens to be very restrictive, which is architecturally the same shape as a target with no DOM at all.

Nothing about the authoring layer changes. The same Column, Text, Image and theme tokens, the same Build(context).

What already exists

Two pieces are in the tree today, verified by reading them:

  • WebRealizer.Lower(node, theme, typeScale, styles: null) — without a style sink, styles stay inline on the element instead of becoming deduplicated atomic classes. Inline style is email's hardest constraint, and this path already produces it.
  • HtmlRenderer.RenderNode(...) turns the lowered tree into an HTML string.

So "C# component → HTML with inline styles" is two existing calls. What is missing is everything about the medium.

What email actually demands

Demand Why Where the web realizer differs
Nested <table> layout Outlook on Windows renders with Word's engine: no flexbox, no grid Row/Column lower to FlexDirection
Inline style="" only <style> blocks and external CSS are stripped or ignored by several clients Atomic classes + a stylesheet (the sink path)
Literal colors CSS custom properties are not resolved Tokens emit var(--eq-color-…, fallback)
Absolute image URLs or CID data: URIs are dropped by Gmail Assets assume a served origin
A constrained width ~600px is the convention every client tolerates Pages are fluid
No interaction No JS, no hover in most clients, no scrolling containers Pressable, ScrollView, hover states exist

Design

A new package, eQuantic.UI.Email, depending only on Primitives and Core — never on eQuantic.UI.Web, so the two realizers cannot drift into each other.

// Authored exactly like a page.
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://…"),
        ]);
}

// Rendered where it is sent.
EmailMessage message = EmailRenderer.Render(new WelcomeEmail("Edgar"), theme);
// message.Html          — table-lowered, fully inline
// message.PlainText     — generated from the SAME tree, not written twice

EmailMessage carries both parts because a multipart message needs both, and generating the text alternative from the tree is the only way it stays in step with the HTML.

Slices

M0 — Measure before building. Take a component that already exists, render it through the two calls above, and open the result in Gmail (web, iOS, Android), Outlook (Windows desktop, web) and Apple Mail. Produce a table of what survives untouched. This decides how big M1 is, and it is half a day. No new code.

M1 — Table lowering. Box, Row, Column and Gap become nested <table>/<tr>/<td> with cell padding and spacer rows. Alignment maps to align/valign. This is the bulk of the track.

M2 — Inline styles and literal theming. Reuse the no-sink path; resolve every token to a literal color and length so nothing depends on custom properties. Density and type scale resolve once, at render.

M3 — Document shell. EmailDocument: doctype, <meta> set, the 600px container, the invisible preheader line, and the dark-mode meta clients honour.

M4 — Vocabulary fence. Diagnostics naming what an email cannot do — ScrollView, Pressable, Draggable, hover and focus styling, position: absolute. A Link becomes <a>; a Button becomes a bulletproof <a> styled as a button. An Image without an absolute URL is an error, not a broken picture in someone's inbox.

M5 — Plain-text alternative. Walk the same tree for the text/plain part.

Fences

  • No <style> block, ever, even where a client supports it. One rule beats a matrix of exceptions.
  • No interactivity. An email is a printed page that happens to have links.
  • No client-specific hacks in components. Anything Outlook needs lives in the realizer, never in something an author writes.
  • Attachments and delivery are not ours. The output is HTML plus text; sending is the app's job (MailKit, SES, whatever it already uses).

How it is tested

The same two nets the rest of the framework uses:

  • HTML pins, regenerated on purpose like the transpiled fixtures, so a layout change is visible as a diff rather than a surprise.
  • A client matrix as conformance, run against real clients and recorded — the browser-is-the- arbiter rule applies here even harder, because email clients are further from a spec than browsers are.

Clone this wiki locally