Mjml Email Templates

aaronontheweb/dotnet-skills/skills/mjml-email-templates

作者 aarononthewebe426ed93a9f3無授權條款1.2K 個星標收錄於 2026年10月8日更新於 2026年10月8日儲存庫3 週前更新

Build responsive email templates using MJML markup language. Compiles to cross-client HTML that works in Outlook, Gmail, and Apple Mail. Includes template renderer, layout patterns, and variable substitution.

AI 產生的概覽

建置可編譯為跨用戶端 HTML 的響應式 MJML 郵件範本,包含 .NET 渲染器與變數替換。

功能
此技能指導使用 MJML 標記語言撰寫響應式郵件範本,編譯後產生以表格為基礎的 HTML,可在 Outlook、Gmail 與 Apple Mail 中一致呈現。內容涵蓋共用版面範本、交易型郵件內容範本、具變數替換的 .NET 範本渲染器、郵件組裝模式,以及開發預覽端點。此外也列出 MJML 元件參考與範本設計最佳實務。
適用情境
適用於建置註冊邀請、密碼重設、收據或通知等交易型郵件。適合在 .NET 中架設 MJML 範本渲染,且需要郵件在主流郵件用戶端中正確顯示的團隊。
執行需求
需要 Mjml.Net NuGet 套件與 .NET 執行階段;範本以資源形式內嵌於專案。此技能不隨附指令碼,除一般套件還原外無需認證或網路存取。

MJML Email Templates

When to Use This Skill

Use this skill when:

  • Building transactional emails (signup, password reset, invoices, notifications)
  • Creating responsive email templates that work across clients
  • Setting up MJML template rendering in .NET

Related skills:

  • aspire/mailpit-integration - Test emails locally with Mailpit
  • testing/verify-email-snapshots - Snapshot test rendered HTML

Why MJML?

Problem: Email HTML is notoriously difficult. Each email client (Outlook, Gmail, Apple Mail) renders differently, requiring complex table-based layouts and inline styles.

Solution: MJML is a markup language that compiles to responsive, cross-client HTML:

mjml
<!-- MJML - simple and readable --><mj-section>  <mj-column>    <mj-text>Hello {{UserName}}</mj-text>    <mj-button href="{{ActionUrl}}">Click Here</mj-button>  </mj-column></mj-section>

Compiles to ~200 lines of table-based HTML with inline styles that works everywhere.


Installation

Add Mjml.Net

bash
dotnet add package Mjml.Net

Embed Templates as Resources

In your .csproj:

xml
<ItemGroup>  <EmbeddedResource Include="Templates\**\*.mjml" /></ItemGroup>

Project Structure

src/  Infrastructure/    MyApp.Infrastructure.Mailing/      Templates/        _Layout.mjml              # Shared layout (header, footer)        UserInvitations/          UserSignupInvitation.mjml          InvitationExpired.mjml        PasswordReset/          PasswordReset.mjml        Billing/          PaymentReceipt.mjml          RenewalReminder.mjml      Mjml/        IMjmlTemplateRenderer.cs        MjmlTemplateRenderer.cs        MjmlEmailMessage.cs      Composers/        IUserEmailComposer.cs        UserEmailComposer.cs      MyApp.Infrastructure.Mailing.csproj

Layout Template (_Layout.mjml)

mjml
<mjml>  <mj-head>    <mj-title>MyApp</mj-title>    <mj-preview>{{PreviewText}}</mj-preview>    <mj-attributes>      <mj-all font-family="'Helvetica Neue', Helvetica, Arial, sans-serif" />      <mj-text font-size="14px" color="#555555" line-height="20px" />      <mj-section padding="20px" />    </mj-attributes>    <mj-style inline="inline">      a { color: #2563eb; text-decoration: none; }      a:hover { text-decoration: underline; }    </mj-style>  </mj-head>  <mj-body background-color="#f3f4f6">    <!-- Header -->    <mj-section background-color="#ffffff" padding-bottom="0">      <mj-column>        <mj-image          src="https://myapp.com/logo.png"          alt="MyApp"          width="150px"          href="{{SiteUrl}}"          padding="30px 25px 20px 25px" />      </mj-column>    </mj-section>
    <!-- Content injected here -->    <mj-section background-color="#ffffff" padding-top="20px" padding-bottom="40px">      <mj-column>        {{Content}}      </mj-column>    </mj-section>
    <!-- Footer -->    <mj-section background-color="#f9fafb" padding="20px 25px">      <mj-column>        <mj-text align="center" font-size="12px" color="#9ca3af">          &copy; 2025 MyApp Inc. All rights reserved.        </mj-text>      </mj-column>    </mj-section>  </mj-body></mjml>

Content Template

mjml
<!-- UserInvitations/UserSignupInvitation.mjml --><!-- Wrapped in _Layout.mjml automatically -->
<mj-text font-size="16px" color="#111827" font-weight="600" padding-bottom="20px">  You've been invited to join {{OrganizationName}}</mj-text>
<mj-text padding-bottom="15px">  Hi {{InviteeName}},</mj-text>
<mj-text padding-bottom="15px">  {{InviterName}} has invited you to join <strong>{{OrganizationName}}</strong>.</mj-text>
<mj-text padding-bottom="25px">  Click the button below to accept your invitation:</mj-text>
<mj-button background-color="#2563eb" color="#ffffff" font-size="16px" href="{{InvitationLink}}">  Accept Invitation</mj-button>
<mj-text padding-top="25px" font-size="13px" color="#6b7280">  This invitation expires on {{ExpirationDate}}.</mj-text>

Template Renderer

csharp
public interface IMjmlTemplateRenderer{    Task<string> RenderTemplateAsync(        string templateName,        IReadOnlyDictionary<string, string> variables,        CancellationToken ct = default);}
public sealed partial class MjmlTemplateRenderer : IMjmlTemplateRenderer{    private readonly MjmlRenderer _mjmlRenderer = new();    private readonly Assembly _assembly;    private readonly string _siteUrl;
    public MjmlTemplateRenderer(IConfiguration config)    {        _assembly = typeof(MjmlTemplateRenderer).Assembly;        _siteUrl = config["SiteUrl"] ?? "https://myapp.com";    }
    public async Task<string> RenderTemplateAsync(        string templateName,        IReadOnlyDictionary<string, string> variables,        CancellationToken ct = default)    {        // Load content template        var contentMjml = await LoadTemplateAsync(templateName, ct);
        // Load layout and inject content        var layoutMjml = await LoadTemplateAsync("_Layout", ct);        var combinedMjml = layoutMjml.Replace("{{Content}}", contentMjml);
        // Merge variables (layout + template-specific)        var allVariables = new Dictionary<string, string>        {            { "SiteUrl", _siteUrl }        };        foreach (var kvp in variables)            allVariables[kvp.Key] = kvp.Value;
        // Substitute variables        var processedMjml = SubstituteVariables(combinedMjml, allVariables);
        // Compile to HTML        var result = await _mjmlRenderer.RenderAsync(processedMjml, null, ct);
        if (result.Errors.Any())            throw new InvalidOperationException(                $"MJML compilation failed: {string.Join(", ", result.Errors.Select(e => e.Error))}");
        return result.Html;    }
    private async Task<string> LoadTemplateAsync(string templateName, CancellationToken ct)    {        var resourceName = $"MyApp.Infrastructure.Mailing.Templates.{templateName.Replace('/', '.')}.mjml";
        await using var stream = _assembly.GetManifestResourceStream(resourceName)            ?? throw new FileNotFoundException($"Template '{templateName}' not found");
        using var reader = new StreamReader(stream);        return await reader.ReadToEndAsync(ct);    }
    private static string SubstituteVariables(string mjml, IReadOnlyDictionary<string, string> variables)    {        return VariableRegex().Replace(mjml, match =>        {            var name = match.Groups[1].Value;            return variables.TryGetValue(name, out var value) ? value : match.Value;        });    }
    [GeneratedRegex(@"\{\{([^}]+)\}\}", RegexOptions.Compiled)]    private static partial Regex VariableRegex();}

Email Composer Pattern

Separate template rendering from email composition with strongly-typed value objects:

csharp
public interface IUserEmailComposer{    Task<EmailMessage> ComposeSignupInvitationAsync(        EmailAddress recipientEmail,        PersonName recipientName,        PersonName inviterName,        OrganizationName organizationName,        AbsoluteUri invitationUrl,        DateTimeOffset expiresAt,        CancellationToken ct = default);}
public sealed class UserEmailComposer : IUserEmailComposer{    private readonly IMjmlTemplateRenderer _renderer;
    public UserEmailComposer(IMjmlTemplateRenderer renderer)    {        _renderer = renderer;    }
    public async Task<EmailMessage> ComposeSignupInvitationAsync(        EmailAddress recipientEmail,        PersonName recipientName,        PersonName inviterName,        OrganizationName organizationName,        AbsoluteUri invitationUrl,        DateTimeOffset expiresAt,        CancellationToken ct = default)    {        var variables = new Dictionary<string, string>        {            { "PreviewText", $"You've been invited to join {organizationName.Value}" },            { "InviteeName", recipientName.Value },            { "InviterName", inviterName.Value },            { "OrganizationName", organizationName.Value },            { "InvitationLink", invitationUrl.ToString() },            { "ExpirationDate", expiresAt.ToString("MMMM d, yyyy") }        };
        var html = await _renderer.RenderTemplateAsync(            "UserInvitations/UserSignupInvitation",            variables,            ct);
        return new EmailMessage(            To: recipientEmail,            Subject: $"You've been invited to join {organizationName.Value}",            HtmlBody: html);    }}

Email Preview Endpoint

Add an admin endpoint to preview emails during development:

csharp
app.MapGet("/admin/emails/preview/{template}", async (    string template,    IMjmlTemplateRenderer renderer) =>{    var sampleVariables = GetSampleVariables(template);    var html = await renderer.RenderTemplateAsync(template, sampleVariables);
    return Results.Content(html, "text/html");}).RequireAuthorization("AdminOnly");

Best Practices

Template Design

mjml
<!-- DO: Use MJML components for layout --><mj-section>  <mj-column>    <mj-text>Content</mj-text>  </mj-column></mj-section>
<!-- DON'T: Use raw HTML tables --><table><tr><td>Content</td></tr></table>
<!-- DO: Use production URLs for images --><mj-image src="https://myapp.com/logo.png" />
<!-- DON'T: Use relative paths --><mj-image src="/img/logo.png" />

Variable Handling

csharp
// DO: Use strongly-typed value objectsTask<EmailMessage> ComposeAsync(    EmailAddress to,    PersonName name,    AbsoluteUri actionUrl);
// DON'T: Use raw stringsTask<EmailMessage> ComposeAsync(    string email,    string name,    string url);

MJML Components Reference

ComponentPurpose
<mj-section>Horizontal container (like a row)
<mj-column>Vertical container within section
<mj-text>Text content with styling
<mj-button>Call-to-action button
<mj-image>Responsive image
<mj-divider>Horizontal line
<mj-spacer>Vertical spacing
<mj-table>Data tables
<mj-social>Social media icons

Resources

來源與署名

來源:aaronontheweb/dotnet-skills位於skills/mjml-email-templates提交e426ed9

授權條款: 無授權條款

內容歸原作者所有。SourceWeft 從公開儲存庫中收錄這些內容。

檢舉或申請下架