Skip to main content
skill CX Codex

mjml-email-templates — Claude Code skill: MJML emails

A skill that gets your agent writing transactional emails in MJML and compiling them to HTML that holds up in Outlook, Gmail and Apple Mail.

Install into Claude Code · Skills CLI (Vercel) All methods and platforms ↓
npx skills add Aaronontheweb/dotnet-skills --skill mjml-email-templates -a claude-code
Open on GitHub

What mjml-email-templates does

The skill kicks in when you build signup, password reset, invoice or notification emails. Instead of hand-writing tables and inline styles, the agent writes MJML markup and the compiler turns it into cross-client HTML. It ships a template renderer, layout patterns and variable substitution with placeholders like {{UserName}}.

The structure is a shared _Layout.mjml with header and footer, plus one file per email type injected into the {{Content}} placeholder. Templates live as EmbeddedResource entries in your .csproj; the MjmlTemplateRenderer reads them from the assembly, substitutes variables and returns finished HTML. On .NET you need the Mjml.Net package — nothing compiles without it.

Compilation errors are not swallowed: bad MJML throws an exception carrying the error text. That is a reason to render a template before you wire it to a send call, not after. The skill points at two neighbours for follow-up work — testing mail locally with Mailpit and snapshot-testing the rendered HTML.

What it helps with

  • build an email that does not fall apart in Outlook
  • assemble transactional emails from a shared header-and-footer layout
  • inject a user name and action link into an email template
  • keep templates as embedded assembly resources instead of loose files
  • catch MJML compilation errors before the email is sent

Example prompts for the agent

  • create an MJML invite email with an accept button and an expiry date
  • add an MJML template renderer to my .NET project and pull in Mjml.Net
  • move the shared email shell into _Layout.mjml and keep content in separate files

How to install mjml-email-templates

1Skills CLI (Vercel)

npx skills add Aaronontheweb/dotnet-skills --skill mjml-email-templates -a claude-code

2Manually: put the files in place

~/.claude/skills/mjml-email-templates/SKILL.md

Copy the whole skill folder; for one project use .claude/skills/mjml-email-templates/

Step by step, with commands for each platform: How to install skills in Claude Code: five ways

Questions about mjml-email-templates

How do I install mjml-email-templates in Claude?

Run npx skills add Aaronontheweb/dotnet-skills --skill mjml-email-templates -a claude-code. The skill lands at ~/.claude/skills/mjml-email-templates/SKILL.md.

What does mjml-email-templates do?

It helps the agent write emails in MJML markup and compile them to HTML for Outlook, Gmail and Apple Mail. It includes a template renderer, layout patterns and variable substitution.

What do I need for the skill to work?

A .NET project and the Mjml.Net package for compilation. Templates are wired up as EmbeddedResource entries in your .csproj.

Source text

SKILL.md · MIT
---
name: mjml-email-templates
description: 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.
user-invocable: false
---

# 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
<!-- 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 objects
Task<EmailMessage> ComposeAsync(
    EmailAddress to,
    PersonName name,
    AbsoluteUri actionUrl);

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

---

## MJML Components Reference

| Component | Purpose |
|-----------|---------|
| `<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

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

© Aaronontheweb · MIT · Aaronontheweb/dotnet-skills

Similar skills

Everything for Frontend and UI →

From the same set32

testcontainers-integration-tests Helps AI agents write .NET integration tests using xUnit and real Docker containers instead of mocks. Useful for verifying database migrations, SQL queries, Redis cache, and message queues. CX Codex +1 544installs playwright-blazor-testing A skill that walks your agent through Playwright end-to-end tests for Blazor Server and WebAssembly: navigation, forms, auth, selectors. CX Codex +1 537installs dotnet-local-tools A skill that walks through per-repository .NET CLI tools via .config/dotnet-tools.json, so local dev and CI restore the same versions. CX Codex +1 526installs aspire-integration-testing A skill for writing .NET Aspire integration tests with xUnit: fixtures, distributed application startup, endpoint discovery. CX Codex +1 499installs aspire-configuration Makes the Aspire AppHost emit plain config keys so application code stays free of Aspire clients and service discovery. CX Codex +1 463installs snapshot-testing A skill that gets your agent writing Verify snapshot tests: output is compared against a baseline you approve by hand. CX Codex +1 461installs skills-index-snippets Builds a compact router of skills and agents for AGENTS.md or CLAUDE.md, so the assistant invokes the right skill by name instead of guessing. CX Codex +1 435installs playwright-ci-caching Caches Playwright browser binaries in GitHub Actions and Azure DevOps pipelines. Eliminates the ~400MB download on every CI run, cutting 1-2 minutes off test workflows. CX Codex +1 434installs

Project sponsors

Companies that keep this analytics open
Gold sponsor

Impressions, clicks and code copies — open statistics. We keep the numbers public for the same reason NeBlask itself is public. Become a sponsor