Skip to content

feat: render email templates with Razor components - #2525

Open
ejsmith wants to merge 10 commits into
mainfrom
feature/razor-email-templates
Open

feat: render email templates with Razor components#2525
ejsmith wants to merge 10 commits into
mainfrom
feature/razor-email-templates

Conversation

@ejsmith

@ejsmith ejsmith commented Aug 25, 2026

Copy link
Copy Markdown
Member

Summary

Replaces the Foundation for Emails/Gulp source project and the generated, embedded Handlebars HTML with a .NET 10 Razor class library used directly by Exceptionless.Core.

  • migrates all 9 production email templates to strongly typed Razor components and record models
  • centralizes the email-safe layout, buttons, sections, callouts, metrics, stack lists, and footers
  • renders templates in process with ASP.NET Core HtmlRenderer
  • removes the npm/Gulp/Foundation source pipeline, generated HTML, embedded template resources, and the Handlebars.Net dependency
  • adds no PackageReference to the email-template project; it uses the shared Microsoft.AspNetCore.App framework
  • adds a file-based .NET preview tool that renders 13 representative variants to a static gallery and can also send them over SMTP to local Mailpit
  • centralizes all 20 existing application destinations in EmailAppUrlBuilder
  • preserves the email security and rendering fixes from test(mail): characterize production email contracts #2526 while retaining that PR's characterization coverage

Behavior

Mailer creates a strongly typed record for each email, renders it through IEmailTemplateRenderer, and then enqueues the existing MailMessage payload. Renderer dispatch is centralized, and generic component constraints verify each component-to-model pairing at compile time. Razor HTML-encodes interpolated content by default; application URLs and mailto: values are constructed and escaped explicitly. Mailbox addresses are parsed and canonicalized before their local and domain components are escaped separately, preserving valid mailbox syntax without allowing message-body query injection.

Email action metadata is emitted as JSON-LD with source-generated System.Text.Json metadata, so subjects containing quotes cannot alter the structured-data document. The queued message shape and delivery pipeline are unchanged. Mailer has one renderer-aware constructor; all construction sites are controlled in this repository and use DI or explicitly provide the renderer.

Application links preserve the existing templates' exact BaseURL-plus-route behavior, including the existing stack actions, organization views, project reports, account settings, and authentication pages. With the production BaseURL, these remain clean paths such as /event/{id}; if a configured BaseURL already contains a hash fragment, that fragment is preserved.

Critical presentation styles are inline in the Razor markup. The remaining <style> block contains client resets and responsive media queries that cannot be represented as inline declarations, so there is no CSS-inlining build step.

Rendering parity audit

The current main base (5cb6b5910, including #2526) and this branch were rendered with identical deterministic models. All 13 representative variants were captured in Chromium at 800 px desktop and 375 px mobile: 52 source screenshots and 26 labeled side-by-side comparisons.

Automated comparison of normalized visible text, decoded href values, and parsed JSON-LD found:

  • all links are identical for all 13 variants
  • JSON-LD is structurally identical for all templates that emit it
  • visible text is identical except for deliberate copy corrections in organization notices and payment failure
  • the contact-request rendering is pixel-identical in Chromium at both widths

Text differences

  • organization monthly/hourly notice: removes duplicate to, changes being counting to being counted, and fixes conserns to concerns
  • organization payment failed: changes the verb login to log in

Visual and markup differences

  • the 580 px desktop container, 95% mobile width, palette, typography scale, button padding, borders, and primary content hierarchy are preserved
  • shared buttons change from approximately margin: 0 0 16px to margin: 16px auto 21px; callouts similarly gain top/bottom spacing, so event bodies are about 10-13 px taller in the representative renders
  • the new markup does not apply Foundation's automatic hyphenation; long words wrap at word boundaries instead, most visibly in organization headings on mobile
  • the organization-added and organization-invited social footer keeps the same text, links, colors, desktop two-column layout, and mobile stacked layout, but the simplified tables add mobile vertical spacing and shift wrapping
  • the throttled daily summary intentionally changes four mobile metrics from a cramped four-across row (which clips 98765) to a readable 2x2 grid; this makes that representative mobile message 66 px taller
  • normal daily-summary mobile content is 48 px shorter because of the simplified row/list markup and spacing
  • most actions now use target="_blank"; the logo has explicit dimensions; light-only color-scheme metadata and presentation roles are added
  • the old XHTML Transitional wrapper, empty preheader, Gmail iOS spacer, Margin fallbacks, automatic hyphenation, and extensive Foundation/Outlook compatibility declarations are removed. Chromium parity does not prove Outlook/Gmail-client parity, so those clients remain the main residual rendering risk

Any subsequent change to a template or shared email component in this PR must regenerate the affected desktop and mobile comparison images and update this audit.

Template audit matrix

Template / variants Content and links Rendering result
Contact request Exact Pixel-identical in Chromium
Event notice: new critical, regression Exact Added button/callout spacing; same fields, user block, actions, and metadata
Organization added Exact Shared social-footer spacing/wrapping differences
Organization invited Exact Shared social-footer spacing/wrapping differences
Organization notice: monthly, hourly Copy fixes only; links exact Copy reflows slightly; shared spacing changes
Organization payment failed login -> log in; links exact Copy reflows slightly; shared spacing changes
Project daily summary: normal, throttled/free, unconfigured Exact Shared spacing/list differences; intentional mobile 2x2 throttled metrics
Email verification Exact Shared button spacing only
Password reset Exact Shared button spacing and word wrapping only

Before / after images

Contact request

Desktop

Contact request desktop before and after

Mobile

Contact request mobile before and after
Event notice: new critical

Desktop

New critical event desktop before and after

Mobile

New critical event mobile before and after
Event notice: regression

Desktop

Regressed event desktop before and after

Mobile

Regressed event mobile before and after
Organization added

Desktop

Organization added desktop before and after

Mobile

Organization added mobile before and after
Organization invited

Desktop

Organization invitation desktop before and after

Mobile

Organization invitation mobile before and after
Organization notice: monthly limit

Desktop

Monthly organization limit desktop before and after

Mobile

Monthly organization limit mobile before and after
Organization notice: hourly throttling

Desktop

Hourly organization throttling desktop before and after

Mobile

Hourly organization throttling mobile before and after
Organization payment failed

Desktop

Organization payment failed desktop before and after

Mobile

Organization payment failed mobile before and after
Project daily summary

Desktop

Project daily summary desktop before and after

Mobile

Project daily summary mobile before and after
Project daily summary: throttled/free

Desktop

Throttled project daily summary desktop before and after

Mobile

Throttled project daily summary mobile before and after
Project daily summary: not configured

Desktop

Unconfigured project daily summary desktop before and after

Mobile

Unconfigured project daily summary mobile before and after
Email verification

Desktop

Email verification desktop before and after

Mobile

Email verification mobile before and after
Password reset

Desktop

Password reset desktop before and after

Mobile

Password reset mobile before and after

Previewing

From the repository root, render the static gallery:

dotnet run --no-cache --file build/EmailTemplatePreviews.cs

This writes the ignored gallery and individual messages to artifacts/email-previews. With the Aspire AppHost running, render the gallery and also send all 13 messages to Mailpit:

dotnet run --no-cache --file build/EmailTemplatePreviews.cs -- --send

By default, Aspire exposes Mailpit SMTP on localhost:1026 and its web UI at http://localhost:8026.

Verification

  • dotnet build --no-restore --disable-build-servers -m:1 — 0 warnings, 0 errors
  • dotnet test --project tests/Exceptionless.Tests/Exceptionless.Tests.csproj --no-restore -- --filter-class Exceptionless.Tests.Mail.MailerTests — 35 passed
  • dotnet run --no-cache --file build/EmailTemplatePreviews.cs — 13 variants rendered
  • deterministic DOM audit — decoded links and parsed JSON-LD preserved across all 13 variants; only the documented copy fixes changed visible text
  • Chromium rendering audit — all 13 variants captured at 800 px and 375 px against current main
  • mailer tests cover all 20 existing application destinations, every production template path, concurrent rendering, HTML encoding, quoted-subject JSON-LD validity, hostile mailto: input, valid mailbox syntax, regressed/non-regressed behavior, absence of unresolved Handlebars/Blazor markers, and strict URL/action contracts

Breaking changes

  • Mailer now has one constructor that requires IEmailTemplateRenderer; the old overload was removed because all construction sites are controlled in this repository and have been updated. No HTTP API, queued message, WebSocket, configuration, or storage contract changes.

@ejsmith
ejsmith marked this pull request as ready for review August 25, 2026 04:04

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 71cd0a0585

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/Exceptionless.Core/Mail/Mailer.cs Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 700762c032

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/Exceptionless.Core/Mail/Mailer.cs Outdated
Comment thread src/Exceptionless.Core/Mail/Mailer.cs Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: da692f94c5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/Exceptionless.Core/Mail/Mailer.cs
@ejsmith
ejsmith force-pushed the feature/razor-email-templates branch from 3e84201 to 054a6fd Compare August 27, 2026 15:58

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c824cef6fa

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

private readonly ILogger _logger;

public Mailer(IQueue<MailMessage> queue, FormattingPluginManager pluginManager, ITextSerializer serializer, AppOptions appOptions, TimeProvider timeProvider, ILogger<Mailer> logger)
public Mailer(IQueue<MailMessage> queue, IEmailTemplateRenderer templateRenderer, FormattingPluginManager pluginManager, ITextSerializer serializer, AppOptions appOptions, TimeProvider timeProvider, ILogger<Mailer> logger)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Restore the public six-parameter Mailer constructor

BLOCKER: Replacing the existing public constructor with this renderer-aware signature breaks source and binary compatibility for consumers that directly instantiate Mailer; rebuilt clients no longer compile, and previously compiled clients can fail with MissingMethodException. Fresh evidence after the earlier review thread is that commit c824cef6 removed the restored overload and its compatibility test, so the final change again lacks the original signature; retain it as a delegating overload.

AGENTS.md reference: AGENTS.md:L67-L67

Useful? React with 👍 / 👎.

@github-actions

Copy link
Copy Markdown

Code Coverage

Package Line Rate Branch Rate Complexity Health
Exceptionless.Insulation 37% 35% 286
Exceptionless.Core 76% 67% 10292
Exceptionless.Web 85% 69% 7974
Exceptionless.AppHost 38% 41% 147
Summary 79% (25549 / 32526) 67% (11958 / 17719) 18699

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant