К основному содержанию
скилл CX Codex

mjml-email-templates — скилл для Claude Code: письма на MJML

Скилл учит агента собирать транзакционные письма на MJML и компилировать их в HTML, который одинаково выглядит в Outlook, Gmail и Apple Mail.

Установить в Claude Code · Через Skills CLI (Vercel) Все способы и платформы ↓
npx skills add Aaronontheweb/dotnet-skills --skill mjml-email-templates -a claude-code
Открыть на GitHub

Что делает mjml-email-templates

Скилл включается, когда вы делаете письма для регистрации, сброса пароля, счетов или уведомлений. Агент пишет разметку MJML вместо таблиц и инлайн-стилей, а компилятор превращает её в кросс-клиентный HTML. Внутри есть рендерер шаблонов, паттерны вёрстки и подстановка переменных вида {{UserName}}.

Устроено так: общий каркас _Layout.mjml с шапкой и подвалом, отдельные файлы под каждый тип письма, которые вставляются в плейсхолдер {{Content}}. Шаблоны лежат как EmbeddedResource в .csproj, рендерер MjmlTemplateRenderer читает их из сборки, подставляет переменные и отдаёт готовый HTML. Для .NET нужен пакет Mjml.Net — без него компиляция не пойдёт.

Ошибки компиляции рендерер не глотает: если MJML собран неверно, летит исключение с текстом ошибок. Это повод проверять шаблон до отправки, а не после. В описании скилла есть ссылки на соседние — тестирование писем через Mailpit и snapshot-тесты отрендеренного HTML.

Какие задачи решает

  • свёрстать письмо, которое не разваливается в Outlook
  • собрать транзакционные письма из общего каркаса с шапкой и подвалом
  • подставлять имя пользователя и ссылку в шаблон письма
  • держать шаблоны встроенными ресурсами сборки, а не файлами на диске
  • проверить, что MJML компилируется без ошибок, до отправки

Примеры запросов агенту

  • сделай шаблон письма-приглашения на MJML с кнопкой принятия и сроком действия
  • добавь рендерер MJML-шаблонов в проект на .NET и подключи Mjml.Net
  • вынеси общий каркас письма в _Layout.mjml, а содержимое — в отдельные файлы

Как установить mjml-email-templates

1Через Skills CLI (Vercel)

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

2Вручную: положить файлы

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

Папка скилла целиком; для одного проекта — .claude/skills/mjml-email-templates/

По шагам, с командами для каждой платформы: Как установить скилл в Claude Code: пять способов

Из того же набора32

testcontainers-integration-tests Скилл учит агента писать интеграционные тесты на .NET и xUnit с реальными базами и очередями в Docker вместо моков. Помогает проверять миграции, SQL-запросы и кэш без ложной уверенности. CX Codex +1 544установок playwright-blazor-testing Скилл учит агента писать end-to-end тесты для Blazor Server и WebAssembly на Playwright: навигация, формы, авторизация, селекторы. CX Codex +1 537установок dotnet-local-tools Скилл объясняет, как вести локальные .NET CLI-инструменты через .config/dotnet-tools.json, чтобы у команды и в CI были одинаковые версии. CX Codex +1 526установок aspire-integration-testing Скилл для написания интеграционных тестов приложений .NET Aspire на xUnit: фикстуры, поднятие distributed application, поиск эндпоинтов. CX Codex +1 499установок aspire-configuration Учит AppHost переводить ресурсы Aspire в явные ключи конфигурации, чтобы код приложения не зависел от клиентов Aspire и service discovery. CX Codex +1 463установок snapshot-testing Скилл учит агента писать снапшот-тесты на Verify: вывод сравнивается с эталонным файлом, который вы утвердили вручную. CX Codex +1 461установок skills-index-snippets Собирает компактный роутер скиллов и агентов для AGENTS.md или CLAUDE.md, чтобы ассистент вызывал нужный скилл по имени, а не по памяти. CX Codex +1 435установок playwright-ci-caching Настраивает кэширование браузеров Playwright в GitHub Actions и Azure DevOps. Устраняет лишнюю загрузку ~400MB бинарников и сокращает сборку на 1-2 минуты в .NET-проектах. CX Codex +1 434установок

Похожие

email-html-mjml Framix Скилл собирает адаптивные шаблоны писем на MJML 5.x и компилирует их в HTML, который переживает Outlook и Gmail. Пригодится, когда нужен welcome, рассылка или транзакционное письмо. CX Codex +1 945установок recipe-email-drive-link официальный googleworkspace Скилл делит файл Google Drive с нужным человеком и отправляет ему письмо со ссылкой. Пригодится, когда отчёты и документы уходят клиентам вручную. CX Codex +1 31 500установок recipe-save-email-attachments официальный googleworkspace Скилл находит письма с вложениями в Gmail и сохраняет эти файлы в выбранную папку на Google Drive. CX Codex +1 31 100установок designing-email-templates PostHog Скилл создаёт и редактирует шаблоны писем для кампаний и рабочих процессов PostHog через MCP. Пригодится, когда письмо нужно собрать из редактируемых блоков и персонализировать с помощью Liquid. CX Codex +1 301установок email-template-builder Alireza Rezvani Создаёт систему транзакционных писем на React Email или MJML, подключает провайдеров и добавляет предпросмотр. Подходит для новых шаблонов, миграции и локализации писем. 602установок email-render-builder aaron-he-zhu Собирает HTML-вёрстку письма из готового креатива и проверяет, как она отрисуется в Gmail, Outlook, Apple Mail и на мобильных. CX Codex +1 1 000установок

Рядом в теме «Фронтенд и UI»

Всё по теме «Фронтенд и UI» →

Вопросы о mjml-email-templates

Как установить mjml-email-templates в Claude?

Выполните npx skills add Aaronontheweb/dotnet-skills --skill mjml-email-templates -a claude-code. Скилл появится по пути ~/.claude/skills/mjml-email-templates/SKILL.md.

Что делает mjml-email-templates?

Помогает агенту писать письма на разметке MJML и компилировать их в HTML для Outlook, Gmail и Apple Mail. Внутри — рендерер шаблонов, паттерны вёрстки и подстановка переменных.

Что нужно, чтобы скилл заработал?

Проект на .NET и пакет Mjml.Net для компиляции. Шаблоны подключаются как EmbeddedResource в .csproj.

Исходный текст

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

Смотрите также

Спонсоры проекта

Компании, которые помогают держать аналитику открытой