$npx -y skills add Aaronontheweb/dotnet-skills --skill mjml-email-templatesBuild 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.
| 1 | # MJML Email Templates |
| 2 | |
| 3 | ## When to Use This Skill |
| 4 | |
| 5 | Use this skill when: |
| 6 | - Building transactional emails (signup, password reset, invoices, notifications) |
| 7 | - Creating responsive email templates that work across clients |
| 8 | - Setting up MJML template rendering in .NET |
| 9 | |
| 10 | **Related skills:** |
| 11 | - `aspire/mailpit-integration` - Test emails locally with Mailpit |
| 12 | - `testing/verify-email-snapshots` - Snapshot test rendered HTML |
| 13 | |
| 14 | --- |
| 15 | |
| 16 | ## Why MJML? |
| 17 | |
| 18 | **Problem**: Email HTML is notoriously difficult. Each email client (Outlook, Gmail, Apple Mail) renders differently, requiring complex table-based layouts and inline styles. |
| 19 | |
| 20 | **Solution**: [MJML](https://mjml.io/) is a markup language that compiles to responsive, cross-client HTML: |
| 21 | |
| 22 | ```mjml |
| 23 | <!-- MJML - simple and readable --> |
| 24 | <mj-section> |
| 25 | <mj-column> |
| 26 | <mj-text>Hello {{UserName}}</mj-text> |
| 27 | <mj-button href="{{ActionUrl}}">Click Here</mj-button> |
| 28 | </mj-column> |
| 29 | </mj-section> |
| 30 | ``` |
| 31 | |
| 32 | Compiles to ~200 lines of table-based HTML with inline styles that works everywhere. |
| 33 | |
| 34 | --- |
| 35 | |
| 36 | ## Installation |
| 37 | |
| 38 | ### Add Mjml.Net |
| 39 | |
| 40 | ```bash |
| 41 | dotnet add package Mjml.Net |
| 42 | ``` |
| 43 | |
| 44 | ### Embed Templates as Resources |
| 45 | |
| 46 | In your `.csproj`: |
| 47 | |
| 48 | ```xml |
| 49 | <ItemGroup> |
| 50 | <EmbeddedResource Include="Templates\**\*.mjml" /> |
| 51 | </ItemGroup> |
| 52 | ``` |
| 53 | |
| 54 | --- |
| 55 | |
| 56 | ## Project Structure |
| 57 | |
| 58 | ``` |
| 59 | src/ |
| 60 | Infrastructure/ |
| 61 | MyApp.Infrastructure.Mailing/ |
| 62 | Templates/ |
| 63 | _Layout.mjml # Shared layout (header, footer) |
| 64 | UserInvitations/ |
| 65 | UserSignupInvitation.mjml |
| 66 | InvitationExpired.mjml |
| 67 | PasswordReset/ |
| 68 | PasswordReset.mjml |
| 69 | Billing/ |
| 70 | PaymentReceipt.mjml |
| 71 | RenewalReminder.mjml |
| 72 | Mjml/ |
| 73 | IMjmlTemplateRenderer.cs |
| 74 | MjmlTemplateRenderer.cs |
| 75 | MjmlEmailMessage.cs |
| 76 | Composers/ |
| 77 | IUserEmailComposer.cs |
| 78 | UserEmailComposer.cs |
| 79 | MyApp.Infrastructure.Mailing.csproj |
| 80 | ``` |
| 81 | |
| 82 | --- |
| 83 | |
| 84 | ## Layout Template (_Layout.mjml) |
| 85 | |
| 86 | ```mjml |
| 87 | <mjml> |
| 88 | <mj-head> |
| 89 | <mj-title>MyApp</mj-title> |
| 90 | <mj-preview>{{PreviewText}}</mj-preview> |
| 91 | <mj-attributes> |
| 92 | <mj-all font-family="'Helvetica Neue', Helvetica, Arial, sans-serif" /> |
| 93 | <mj-text font-size="14px" color="#555555" line-height="20px" /> |
| 94 | <mj-section padding="20px" /> |
| 95 | </mj-attributes> |
| 96 | <mj-style inline="inline"> |
| 97 | a { color: #2563eb; text-decoration: none; } |
| 98 | a:hover { text-decoration: underline; } |
| 99 | </mj-style> |
| 100 | </mj-head> |
| 101 | <mj-body background-color="#f3f4f6"> |
| 102 | <!-- Header --> |
| 103 | <mj-section background-color="#ffffff" padding-bottom="0"> |
| 104 | <mj-column> |
| 105 | <mj-image |
| 106 | src="https://myapp.com/logo.png" |
| 107 | alt="MyApp" |
| 108 | width="150px" |
| 109 | href="{{SiteUrl}}" |
| 110 | padding="30px 25px 20px 25px" /> |
| 111 | </mj-column> |
| 112 | </mj-section> |
| 113 | |
| 114 | <!-- Content injected here --> |
| 115 | <mj-section background-color="#ffffff" padding-top="20px" padding-bottom="40px"> |
| 116 | <mj-column> |
| 117 | {{Content}} |
| 118 | </mj-column> |
| 119 | </mj-section> |
| 120 | |
| 121 | <!-- Footer --> |
| 122 | <mj-section background-color="#f9fafb" padding="20px 25px"> |
| 123 | <mj-column> |
| 124 | <mj-text align="center" font-size="12px" color="#9ca3af"> |
| 125 | © 2025 MyApp Inc. All rights reserved. |
| 126 | </mj-text> |
| 127 | </mj-column> |
| 128 | </mj-section> |
| 129 | </mj-body> |
| 130 | </mjml> |
| 131 | ``` |
| 132 | |
| 133 | --- |
| 134 | |
| 135 | ## Content Template |
| 136 | |
| 137 | ```mjml |
| 138 | <!-- UserInvitations/UserSignupInvitation.mjml --> |
| 139 | <!-- Wrapped in _Layout.mjml automatically --> |
| 140 | |
| 141 | <mj-text font-size="16px" color="#111827" font-weight="600" padding-bottom="20px"> |
| 142 | You've been invited to join {{OrganizationName}} |
| 143 | </mj-text> |
| 144 | |
| 145 | <mj-text padding-bottom="15px"> |
| 146 | Hi {{InviteeName}}, |
| 147 | </mj-text> |
| 148 | |
| 149 | <mj-text padding-bottom="15px"> |
| 150 | {{InviterName}} has invited you to join <strong>{{OrganizationName}}</strong>. |
| 151 | </mj-text> |
| 152 | |
| 153 | <mj-text padding-bottom="25px"> |
| 154 | Click the button below to accept your invitation: |
| 155 | </mj-text> |
| 156 | |
| 157 | <mj-button background-color="#2563eb" color="#ffffff" font-size="16px" href="{{InvitationLink}}"> |
| 158 | Accept Invitation |
| 159 | </mj-button> |
| 160 | |
| 161 | <mj-text padding-top="25px" font-size="13px" color="#6b7280"> |
| 162 | This invitation expires on {{ExpirationDate}}. |
| 163 | </mj-text> |
| 164 | ``` |
| 165 | |
| 166 | --- |
| 167 | |
| 168 | ## Template Renderer |
| 169 | |
| 170 | ```csharp |
| 171 | public interface IMjmlTemplateRenderer |
| 172 | { |
| 173 | Task<string> RenderTemplateAsync( |
| 174 | string templateName, |
| 175 | IReadOnlyDictionary<string, string> variables, |
| 176 | CancellationToken ct = default); |
| 177 | } |
| 178 | |
| 179 | public sealed partial class MjmlTemplateRenderer : IMjmlTemplateRenderer |
| 180 | { |
| 181 | private readonly MjmlRenderer _mjmlRenderer = new(); |
| 182 | private readonly Assembly _assembly; |
| 183 | private readonly string _siteUrl; |
| 184 | |
| 185 | public MjmlTemplateRenderer(IConfiguration config) |
| 186 | { |
| 187 | _assembly = typeof(MjmlTemplateRenderer).Assembly; |
| 188 | _siteUrl = config["SiteUrl"] ?? "https://myapp.com"; |
| 189 | } |
| 190 | |
| 191 | public asy |