# Mjml Email Templates

> 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.

- **Type:** Skill
- **Install:** `agentstack add skill-aaronontheweb-dotnet-skills-mjml-email-templates`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [Aaronontheweb](https://agentstack.voostack.com/s/aaronontheweb)
- **Installs:** 0
- **Category:** [Communication](https://agentstack.voostack.com/c/communication)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [Aaronontheweb](https://github.com/Aaronontheweb)
- **Source:** https://github.com/Aaronontheweb/dotnet-skills/tree/master/skills/mjml-email-templates

## Install

```sh
agentstack add skill-aaronontheweb-dotnet-skills-mjml-email-templates
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# 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](https://mjml.io/) is a markup language that compiles to responsive, cross-client HTML:

```mjml

  
    Hello {{UserName}}
    Click Here
  

```

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

  

```

---

## 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

  
    MyApp
    {{PreviewText}}
    
      
      
      
    
    
      a { color: #2563eb; text-decoration: none; }
      a:hover { text-decoration: underline; }
    
  
  
    
    
      
        
      
    

    
    
      
        {{Content}}
      
    

    
    
      
        
          &copy; 2025 MyApp Inc. All rights reserved.
        
      
    
  

```

---

## Content Template

```mjml

  You've been invited to join {{OrganizationName}}

  Hi {{InviteeName}},

  {{InviterName}} has invited you to join {{OrganizationName}}.

  Click the button below to accept your invitation:

  Accept Invitation

  This invitation expires on {{ExpirationDate}}.

```

---

## Template Renderer

```csharp
public interface IMjmlTemplateRenderer
{
    Task RenderTemplateAsync(
        string templateName,
        IReadOnlyDictionary 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 RenderTemplateAsync(
        string templateName,
        IReadOnlyDictionary 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
        {
            { "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 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 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 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 ComposeSignupInvitationAsync(
        EmailAddress recipientEmail,
        PersonName recipientName,
        PersonName inviterName,
        OrganizationName organizationName,
        AbsoluteUri invitationUrl,
        DateTimeOffset expiresAt,
        CancellationToken ct = default)
    {
        var variables = new Dictionary
        {
            { "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

  
    Content
  

Content

```

### Variable Handling

```csharp
// DO: Use strongly-typed value objects
Task ComposeAsync(
    EmailAddress to,
    PersonName name,
    AbsoluteUri actionUrl);

// DON'T: Use raw strings
Task ComposeAsync(
    string email,
    string name,
    string url);
```

---

## MJML Components Reference

| Component | Purpose |
|-----------|---------|
| `` | Horizontal container (like a row) |
| `` | Vertical container within section |
| `` | Text content with styling |
| `` | Call-to-action button |
| `` | Responsive image |
| `` | Horizontal line |
| `` | Vertical spacing |
| `` | Data tables |
| `` | Social media icons |

---

## Resources

- **MJML Documentation**: https://documentation.mjml.io/
- **MJML Playground**: https://mjml.io/try-it-live
- **Mjml.Net**: https://github.com/ArtZab/Mjml.Net

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [Aaronontheweb](https://github.com/Aaronontheweb)
- **Source:** [Aaronontheweb/dotnet-skills](https://github.com/Aaronontheweb/dotnet-skills)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-aaronontheweb-dotnet-skills-mjml-email-templates
- Seller: https://agentstack.voostack.com/s/aaronontheweb
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
