Summary#
A mail that looks designed has the same few pieces every time: the sender's name at the top, a clear headline, a paragraph or two,
the lines of a receipt with their amounts, one obvious button, and a footer saying who sent it. This kit is those
pieces, written as [[ui-email|[Email]]] parts in the app's own theme.
// app.osy
use Osysharp.EmailTemplates@0;Signature#
EmailLayout(string brand, string? preview = null, string? logo = null, string? address = null,
string? unsubscribe = null, string? footer = null) { … }
EmailHeading(string text)
EmailText(string text)
EmailNote(string text)
EmailButton(string label, string href)
EmailImage(string src, string alt, int width = 552, bool inline = false)
EmailCode(string code)
EmailDivider()
EmailTable() { … } // EmailRow + EmailTotal inside
EmailRow(string label, string value, string? detail = null)
EmailTotal(string label, string value)
// The mail blocks — [Block] [Email] parts staff place in a mail's Region:
EmailWords(Markdown words)
EmailHeadline(string heading, Markdown words = "")
EmailPicture(Picture picture, string alt = "")
EmailAction(string label, PageLink link = "/")
EmailQuote(string quote, string who = "", Color ground = Colors.Muted)
EmailSmallPrint(Markdown words)Description#
The frame: EmailLayout#
EmailLayout is the outside of every mail: the sender's own page — the theme's background and its type — in a
600-pixel column, the width every client lays out well, with the brand at the top, the mail's content under it, and a
footer below a rule. A mail reads like one of the app's pages, not like one of its cards: a shop that is black type on
white sends black type on white, whatever colour its panels are.
brandis the sender's name. It heads the mail unless there is alogo, which replaces it. A logo that is a file the app stores (public/logo.png) travels INSIDE the mail, so the brand shows even where a client blocks remote pictures; see [[ui-email#pictures]].previewis the line an inbox shows beside the subject. Without one, a client shows the mail's first words.addressis where the sender is. A marketing mail must carry a postal address, and a receipt is better for one.unsubscribeis the address that stops this kind of mail. Give it for anything a reader signed up to, and never for a mail they cannot stop receiving, such as a receipt.footeris one more line of small print above the address, such as why the reader is getting this mail.
The parts that go inside#
| part | what it draws |
|---|---|
EmailHeading(text) | the headline, in the theme's display face |
EmailText(text) | a paragraph — a line break in the text is kept |
EmailNote(text) | small print in the muted colour |
EmailButton(label, href) | the one thing to do next — a button that is a link in every client, Outlook included |
EmailImage(src, alt, width, inline) | a picture, 552 pixels wide unless told otherwise (the column's inner width) — by address, or with inline: true a file the app stores, carried inside the mail |
EmailCode(code) | a code to type or read out — a gift code, a return code — large and spaced |
EmailDivider() | a rule between two parts of the mail |
EmailTable() { … } | a receipt's lines, under a rule — EmailRows and an EmailTotal |
EmailRow(label, value, detail) | one line: what it is, with an optional detail under it (a size), and what it costs |
EmailTotal(label, value) | the line a receipt ends on, bold, under a rule |
It wears your theme#
Every colour, size, corner and face in the kit is a theme token: Colors.Primary for the button, Colors.Bg and
Colors.OnBg for the page and its type, FontSize.Title for the headline, Font.Display for its face. The renderer writes the app's value
for each, so an app that declares theme Brand { Colors { Primary = "#C2410C"; } } sends orange buttons without
touching the kit.
A part of your own#
Each part is an ordinary [Email] component over the platform's email atoms, so a different one is written the same
way. osy get EmailButton copies the kit's into your app as a starting point.
The mail blocks — what staff arrange#
A mail's words that staff own are a [[ui-email#region|region]] of blocks, arranged at the desk like a page's. The kit
ships the everyday ones; each is an [Email] part marked [Block], in the app's theme, its words written with
placeholders: true so they are filled from whichever mail they are placed in:
| block | what staff fill in |
|---|---|
EmailWords — Words | a paragraph in Markdown — what the mail says first |
EmailHeadline — Heading and words | a heading, and a paragraph under it |
EmailPicture — Picture | a photograph across the mail — one the app ships, or one uploaded — and what it shows |
EmailAction — Button | what it says, and where it goes (a page of the app, or an address) |
EmailQuote — Quote | what somebody said, who said it, and one of the theme's grounds to set it on |
EmailSmallPrint — Small print | a quiet line at the foot of the mail |
Region("welcome", allow: [EmailWords, EmailHeadline, EmailPicture, EmailAction, EmailQuote, EmailSmallPrint]) {
EmailWords(words: "Welcome aboard, **{member.name}**.");
}A region needs use Osysharp.Blocks; in the app — that is where what staff arrange is kept. An app that arranges
nothing uses the same components as ordinary parts, and needs nothing more: EmailQuote("…", who: "…") in a template
is a quote.
A save of words naming a placeholder the mail does not fill is refused by name (Osysharp.Blocks' WordsProblem),
and PlaceholdersIn, UnknownPlaceholders and PlaceholderRefusal — the same check over any words, a campaign's body
say — are there too.
Examples#
A receipt, from the order it confirms#
app EmailKitExample {
use Osysharp.EmailTemplates;
model "model/**/*.osy";
}
using Osysharp.EmailTemplates;
entity Sale {
[Required, MaxLength(40)] string Reference;
[Required, MaxLength(120)] string Customer;
[ForeignKey(Sale)] SaleLine[] Lines;
security { allow read, create when IsAuthenticated; }
}
entity SaleLine {
[Required] Sale Sale;
[Required, MaxLength(120)] string Name;
[MaxLength(60)] string? Size;
int Quantity;
decimal Price;
security { allow read, create when IsAuthenticated; }
}
string Kronor(decimal amount) { return $"{amount:0.00} kr"; }
[Email]
component SaleReceipt(Sale sale) {
subject => $"Your receipt {sale.Reference}";
render {
EmailLayout("Grounds & Grapes", preview: $"Thank you, {sale.Customer} — receipt {sale.Reference}",
address: "Storgatan 1, 111 22 Stockholm") {
EmailHeading($"Thank you, {sale.Customer}");
EmailText("Your order is reserved and on its way to being packed.");
EmailTable() {
foreach (var line in sale.Lines) {
EmailRow($"{line.Quantity} × {line.Name}", Kronor(line.Price * line.Quantity), detail: line.Size);
}
EmailTotal("Total", Kronor(sale.Lines.Sum(l => l.Price * l.Quantity)));
}
EmailButton("See your order", $"https://shop.example.com/orders/{sale.Reference}");
EmailNote("Questions? Reply to this mail.");
}
}
}See also#
- [Email] — mail rendered from data — the
[Email]render target: the atoms, what a mail may hold, the text twin, photographing one. - Blocks and regions — pages the staff arrange and The PageBuilder kit — the desk staff arrange the site at — how a region's blocks are arranged, and the desk that shows the mail beside them.
- theme tokens — the tokens the kit's colours and sizes come from, and how an app overrides them.