O que Modelos de Email MJML faz?
Quando Usar Esta Skill
Use esta skill quando:
- Criando emails transacionais (cadastro, redefinição de senha, faturas, notificações)
- Criando modelos de email responsivos que funcionam em vários clientes
- Configurando a renderização de modelos MJML em .NET
Skills relacionadas:
aspire/mailpit-integration- Teste emails localmente com Mailpittesting/verify-email-snapshots- Teste de snapshot do HTML renderizado
Por que MJML?
Problema: O HTML de email é notoriamente difícil. Cada cliente de email (Outlook, Gmail, Apple Mail) renderiza de forma diferente, exigindo layouts complexos baseados em tabelas e estilos inline.
Solução: MJML é uma linguagem de marcação que compila para HTML responsivo e compatível com vários clientes:
<!-- MJML - simples e legível -->
<mj-section>
<mj-column>
<mj-text>Olá {{NomeUsuario}}</mj-text>
<mj-button href="{{UrlAcao}}">Clique Aqui</mj-button>
</mj-column>
</mj-section>
Compila para ~200 linhas de HTML baseado em tabelas com estilos inline que funcionam em todos os lugares.
Instalação
Adicionar Mjml.Net
dotnet add package Mjml.Net
Incorporar Modelos como Recursos
No seu .csproj:
<ItemGroup>
<EmbeddedResource Include="Templates\**\*.mjml" />
</ItemGroup>
Estrutura do Projeto
src/
Infrastructure/
MyApp.Infrastructure.Mailing/
Templates/
_Layout.mjml # Layout compartilhado (cabeçalho, rodapé)
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
Modelo de Layout (_Layout.mjml)
<mjml>
<mj-head>
<mj-title>MyApp</mj-title>
<mj-preview>{{TextoPreview}}</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">
<!-- Cabeçalho -->
<mj-section background-color="#ffffff" padding-bottom="0">
<mj-column>
<mj-image
src="https://myapp.com/logo.png"
alt="MyApp"
width="150px"
href="{{UrlSite}}"
padding="30px 25px 20px 25px" />
</mj-column>
</mj-section>
<!-- Conteúdo injetado aqui -->
<mj-section background-color="#ffffff" padding-top="20px" padding-bottom="40px">
<mj-column>
{{Content}}
</mj-column>
</mj-section>
<!-- Rodapé -->
<mj-section background-color="#f9fafb" padding="20px 25px">
<mj-column>
<mj-text align="center" font-size="12px" color="#9ca3af">
© 2025 MyApp Inc. Todos os direitos reservados.
</mj-text>
</mj-column>
</mj-section>
</mj-body>
</mjml>
Modelo de Conteúdo
<!-- UserInvitations/UserSignupInvitation.mjml -->
<!-- Embrulhado em _Layout.mjml automaticamente -->
<mj-text font-size="16px" color="#111827" font-weight="600" padding-bottom="20px">
Você foi convidado para entrar em {{NomeOrganizacao}}
</mj-text>
<mj-text padding-bottom="15px">
Olá {{NomeConvidado}},
</mj-text>
<mj-text padding-bottom="15px">
{{NomeConvidante}} convidou você para entrar em <strong>{{NomeOrganizacao}}</strong>.
</mj-text>
<mj-text padding-bottom="25px">
Clique no botão abaixo para aceitar seu convite:
</mj-text>
<mj-button background-color="#2563eb" color="#ffffff" font-size="16px" href="{{LinkConvite}}">
Aceitar Convite
</mj-button>
<mj-text padding-top="25px" font-size="13px" color="#6b7280">
Este convite expira em {{DataExpiracao}}.
</mj-text>
Renderizador de Modelos
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)
{
// Carregar modelo de conteúdo
var contentMjml = await LoadTemplateAsync(templateName, ct);
// Carregar layout e injetar conteúdo
var layoutMjml = await LoadTemplateAsync("_Layout", ct);
var combinedMjml = layoutMjml.Replace("{{Content}}", contentMjml);
// Mesclar variáveis (layout + específicas do modelo)
var allVariables = new Dictionary<string, string>
{
{ "SiteUrl", _siteUrl }
};
foreach (var kvp in variables)
allVariables[kvp.Key] = kvp.Value;
// Substituir variáveis
var processedMjml = SubstituteVariables(combinedMjml, allVariables);
// Compilar para HTML
var result = await _mjmlRenderer.RenderAsync(processedMjml, null, ct);
if (result.Errors.Any())
throw new InvalidOperationException(
$"Falha na compilação MJML: {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($"Modelo '{templateName}' não encontrado");
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();
}
Padrão de Compositor de Email
Separe a renderização do modelo da composição do email com objetos de valor fortemente tipados:
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", $"Você foi convidado para entrar em {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: $"Você foi convidado para entrar em {organizationName.Value}",
HtmlBody: html);
}
}
Endpoint de Visualização de Email
Adicione um endpoint de administração para visualizar emails durante o desenvolvimento:
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");
Melhores Práticas
Design de Modelo
<!-- FAÇA: Use componentes MJML para layout -->
<mj-section>
<mj-column>
<mj-text>Conteúdo</mj-text>
</mj-column>
</mj-section>
<!-- NÃO FAÇA: Use tabelas HTML brutas -->
<table><tr><td>Conteúdo</td></tr></table>
<!-- FAÇA: Use URLs de produção para imagens -->
<mj-image src="https://myapp.com/logo.png" />
<!-- NÃO FAÇA: Use caminhos relativos -->
<mj-image src="/img/logo.png" />
Tratamento de Variáveis
// FAÇA: Use objetos de valor fortemente tipados
Task<EmailMessage> ComposeAsync(
EmailAddress to,
PersonName name,
AbsoluteUri actionUrl);
// NÃO FAÇA: Use strings brutas
Task<EmailMessage> ComposeAsync(
string email,
string name,
string url);
Referência de Componentes MJML
| Componente | Propósito |
|---|---|
<mj-section> | Contêiner horizontal (como uma linha) |
<mj-column> | Contêiner vertical dentro da seção |
<mj-text> | Conteúdo de texto com estilo |
<mj-button> | Botão de call-to-action |
<mj-image> | Imagem responsiva |
<mj-divider> | Linha horizontal |
<mj-spacer> | Espaçamento vertical |
<mj-table> | Tabelas de dados |
<mj-social> | Ícones de redes sociais |
Recursos
- Documentação MJML: https://documentation.mjml.io/
- Playground MJML: https://mjml.io/try-it-live
- Mjml.Net: https://github.com/ArtZab/Mjml.Net