Skip to content

feat: modernize email templates with Svelte 5 + Tailwind + Storybook#2258

Open
niemyjski wants to merge 3 commits into
mainfrom
niemyjski/email-templates-modernization
Open

feat: modernize email templates with Svelte 5 + Tailwind + Storybook#2258
niemyjski wants to merge 3 commits into
mainfrom
niemyjski/email-templates-modernization

Conversation

@niemyjski
Copy link
Copy Markdown
Member

Summary

Modernizes the 8 Exceptionless email templates from a decade-old Foundation for Emails / Gulp / Inky / Panini / SCSS toolchain to Svelte 5 + @better-svelte-email + Tailwind CSS, with zero visual regressions, Storybook for live preview, and a comprehensive security audit.


What Changed

Architecture

  • Replaced: Gulp + Inky + Panini + SCSS + Foundation for Emails → produces static HTML
  • New: Vite + Svelte 5 SSR + @better-svelte-email/server + Tailwind CSS (inlined)
  • Build output: npm run build in src/Exceptionless.EmailTemplates renders all 8 templates to src/Exceptionless.Core/Mail/Templates/*.html
  • C# backend unchanged: Mailer.cs and HandlebarsDotNet rendering pipeline untouched

Security Fixes

CVE Severity Fix
GHSA-crpf-4hrx-3jrp High Svelte 5.34.7 → 5.55.9
GHSA-m56q-vw4c-c2cp High Svelte 5.34.7 → 5.55.9
GHSA-f7gr-6p89-r883 High Svelte 5.34.7 → 5.55.9
GHSA-phwv-c562-gvmh High Svelte 5.34.7 → 5.55.9
GHSA-pr6f-5x2q-rwfp High Svelte 5.34.7 → 5.55.9
GHSA-rcqx-6q8c-2c42 Moderate Svelte 5.34.7 → 5.55.9
XSS via preheader High {@html preheader}{preheader} text binding

Security audit result for @better-svelte-email: SAFE TO ADOPT. Clean dependency tree (parse5, postcss, tailwindcss). SLSA provenance verified. Single-maintainer bus factor noted; versions pinned exactly.

Code Quality (Staff Engineer Review Fixes)

  • Removed all @ts-nocheck from components — proper TypeScript throughout
  • Typed all Snippet props: import type { Snippet } from 'svelte'
  • Replaced all hardcoded hex colors with named Tailwind theme tokens (see src/theme.ts)
  • Fixed parseInt calls to include radix argument
  • Stripped HTML comments from build output (were leaking internal markup)
  • Consolidated fragile split-table {@html} in SocialFooter into single block
  • Removed compilerOptions.generate from vite.config.ts (dropped in Svelte 5)

Critical Bug Fixed

cleanHtml's whitespace collapsing would turn JSON-LD }\n}}}, which HandlebarsDotNet parsed as a Handlebars closing token. Fix: extract <script type="application/ld+json"> blocks before whitespace collapse, restore with newlines after.

New Tooling

  • Storybook 10 with stories for all 8 templates (runs at npm run storybook)
  • ESLint (flat config), Prettier, svelte-check — 0 errors, 0 warnings
  • src/theme.ts: centralized design tokens (primary, dark, alert, muted colors)
  • Scripts: build, dev, check, lint, format, storybook, build-storybook

Visual Parity

All 8 templates verified pixel-perfect via Mailpit + browser screenshots (before/after).

Storybook Screenshots

Password Reset

Email Verify | renders identically to password reset layout

Event Notice — shows error detail, user info, actions

Daily Summary — stats grid, project breakdown

Organization Added

Payment Failed


How to Run

cd src/Exceptionless.EmailTemplates
npm ci
npm run build          # regenerate .html files
npm run storybook      # visual preview at http://localhost:6006
npm run check          # svelte-check: 0 errors
npm run lint           # eslint + prettier: 0 errors

Tests

All 25 mailer tests pass:

dotnet test --project tests/Exceptionless.Tests -- --filter-class Exceptionless.Tests.Mail.MailerTests
# total: 25 | failed: 0 | succeeded: 25

Breaking Changes

None. C# Mailer.cs and all Handlebars templates are fully backward-compatible. The compiled .html output format is identical to the old toolchain.

niemyjski and others added 3 commits May 26, 2026 23:25
Replace the legacy Foundation for Emails (Gulp/Inky/Panini/SCSS) toolchain
with Svelte 5 + @better-svelte-email + Tailwind CSS.

- Migrate all 8 email templates to Svelte components
- Add shared EmailLayout, ActionsFooter, SocialFooter components
- New build system: Vite SSR + @better-svelte-email/server renderer
- Output maintains identical visual appearance and Handlebars tokens
- All 25 mailer tests pass with new template output
- Remove old build tooling (Gulp, Babel, SCSS, Panini, Inky)

The compiled HTML templates preserve Handlebars syntax for runtime
rendering by HandlebarsDotNet in the .NET backend (unchanged).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Migrate 8 email templates from Foundation for Emails / Gulp / Inky / Panini / SCSS
  to @better-svelte-email/server 2.1.1 + Tailwind CSS
- Security audit: Svelte upgraded 5.34.7 → 5.55.9 (patched 6 XSS SSR CVEs)
- Pixel-perfect visual parity: all 8 templates verified with before/after screenshots
- Centralized design tokens in src/theme.ts (named Tailwind colors: text-primary,
  bg-dark, text-alert, etc.) — no more hardcoded hex in .svelte sources
- Fixed XSS: preheader was {@html preheader}, now plain {preheader} text binding
- Add Storybook 10 with stories for all 8 templates + sample data with fillTokens()
- Add ESLint (flat config), Prettier, svelte-check — 0 errors, 25/25 tests pass
- Add @types/node, vite/client types, skipLibCheck for clean type checking
- Fix build script: typed Component, parseInt radix, HTML comment stripping
- Fix JSON-LD '}\n}' → '}}' Handlebars parse collision in cleanHtml
- Remove compilerOptions.generate from vite.config.ts (Svelte 5 no longer supports it)
- Remove old Gulp/Babel/SCSS/Panini/Inky/Foundation toolchain entirely

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Rewrite fillTokens in sample-data.ts as a proper Handlebars evaluator
  supporting if/else/each blocks, nested depth tracking, @index and
  {{../parent}} scope resolution — fixes token concatenation bug where all
  {{#if}} branches were showing simultaneously
- Fix cleanHtml() to replace newlines in text nodes with a space instead of
  removing them (was causing 'fromwhich', 'yourapplication', etc.)
- Fix text typos in organization-notice.svelte: 'to to continue' → 'to continue',
  'to to see' → 'to see', 'being counting' → 'counting'
- Add Storybook (port 6006) and EmailStorybook (port 6008) as AddJavaScriptApp
  resources in Aspire AppHost for integrated development dashboard
- Change email Storybook port to 6008 to avoid conflict with Svelte app (6006)
- Rebuild all 8 generated HTML templates with whitespace fixes applied
- 25/25 mailer tests pass

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@niemyjski
Copy link
Copy Markdown
Member Author

✅ Rendering Bug Fixes — Verified

Latest commit (b5cd6e6) fixes all rendering/formatting issues found in review:

Fixed Issues

Issue Root Cause Fix
All {{#if}} branches showing simultaneously fillTokens used regex (couldn't track nesting) Rewrote as character-by-character evaluator with depth tracking
Extra <hr> before first error field {{#if @index}} — was always truthy with naive regex isTruthy('0') returns false; first item correctly skips divider
"fromwhich" word concatenation cleanHtml removed all newlines incl. text node linebreaks Replace \n' ' (space) not ''
"to to continue/see" double word Typo in template source Fixed in organization-notice.svelte
"?Exceptionless" missing space Same whitespace collapse issue Fixed by cleanHtml fix + single-line in template

Screenshots (after fixes, with realistic sample data)

Event Notice — single clean message, no branch concatenation, no extra HR before first field
Password Reset — "from which" correct spacing
Organization Notice — "to continue", "to see" correct
Organization Invited — "? Exceptionless" with proper space
Daily Summary — MostFrequent + Newest lists, [REGRESSED] badge

Tests

Lint/Type Check

@github-actions
Copy link
Copy Markdown

Code Coverage

Package Line Rate Branch Rate Complexity Health
Exceptionless.Insulation 25% 23% 203
Exceptionless.Web 73% 62% 3920
Exceptionless.AppHost 16% 9% 86
Exceptionless.Core 69% 63% 7777
Summary 68% (13474 / 19806) 61% (7083 / 11530) 11986

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