feat: modernize email templates with Svelte 5 + Tailwind + Storybook - #2258
feat: modernize email templates with Svelte 5 + Tailwind + Storybook#2258niemyjski wants to merge 21 commits into
Conversation
✅ Rendering Bug Fixes — VerifiedLatest commit (b5cd6e6) fixes all rendering/formatting issues found in review: Fixed Issues
Screenshots (after fixes, with realistic sample data)Event Notice — single clean message, no branch concatenation, no extra HR before first field TestsLint/Type Check |
There was a problem hiding this comment.
Pull request overview
This PR replaces the legacy Foundation/Gulp email-template pipeline with a Svelte 5 + Vite + Tailwind-based renderer that generates the static Handlebars HTML consumed by Exceptionless.Core mail delivery.
Changes:
- Adds a new Svelte email-template project with build, lint, check, Storybook preview, and template-rendering scripts.
- Recreates the email templates as Svelte components and checks in regenerated HTML outputs for the .NET mailer.
- Adds AppHost registration for the email Storybook and removes the legacy Foundation/Gulp source structure.
Reviewed changes
Copilot reviewed 66 out of 68 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
.gitignore |
Adds .gstack/ ignore entry. |
src/Exceptionless.AppHost/Program.cs |
Registers component and email Storybook JavaScript apps. |
src/Exceptionless.Core/Mail/Templates/event-notice.html |
Regenerated event notice email output. |
src/Exceptionless.Core/Mail/Templates/organization-added.html |
Regenerated organization added email output. |
src/Exceptionless.Core/Mail/Templates/organization-invited.html |
Regenerated organization invitation email output. |
src/Exceptionless.Core/Mail/Templates/organization-notice.html |
Regenerated organization limit/throttle notice output. |
src/Exceptionless.Core/Mail/Templates/organization-payment-failed.html |
Regenerated payment failed email output. |
src/Exceptionless.Core/Mail/Templates/user-email-verify.html |
Regenerated email verification output. |
src/Exceptionless.Core/Mail/Templates/user-password-reset.html |
Regenerated password reset output. |
src/Exceptionless.EmailTemplates/.babelrc |
Removes legacy Babel config. |
src/Exceptionless.EmailTemplates/.gitignore |
Updates ignores for the new Svelte/Vite project. |
src/Exceptionless.EmailTemplates/.npmrc |
Removes legacy npm release-age config. |
src/Exceptionless.EmailTemplates/.prettierignore |
Adds formatting ignore paths. |
src/Exceptionless.EmailTemplates/.prettierrc |
Adds Prettier configuration. |
src/Exceptionless.EmailTemplates/.storybook/main.ts |
Configures Storybook for email previews. |
src/Exceptionless.EmailTemplates/.storybook/preview.ts |
Adds Storybook preview parameters. |
src/Exceptionless.EmailTemplates/LICENSE |
Removes legacy ZURB license file. |
src/Exceptionless.EmailTemplates/README.md |
Rewrites documentation for the new Svelte email workflow. |
src/Exceptionless.EmailTemplates/eslint.config.js |
Adds ESLint flat config. |
src/Exceptionless.EmailTemplates/example.config.json |
Removes legacy mail/Litmus example config. |
src/Exceptionless.EmailTemplates/gulpfile.babel.js |
Removes legacy Gulp build pipeline. |
src/Exceptionless.EmailTemplates/package.json |
Replaces legacy Foundation dependencies/scripts with Svelte/Vite/Storybook tooling. |
src/Exceptionless.EmailTemplates/src/assets/img/.gitkeep |
Removes unused legacy asset placeholder. |
src/Exceptionless.EmailTemplates/src/assets/scss/_settings.scss |
Removes legacy Foundation email settings. |
src/Exceptionless.EmailTemplates/src/assets/scss/app.scss |
Removes legacy SCSS entrypoint. |
src/Exceptionless.EmailTemplates/src/assets/scss/template/_template.scss |
Removes legacy email template styling. |
src/Exceptionless.EmailTemplates/src/build-emails.ts |
Adds renderer/cleaner/validator that writes generated HTML templates. |
src/Exceptionless.EmailTemplates/src/components/ActionsFooter.svelte |
Adds shared actions footer component. |
src/Exceptionless.EmailTemplates/src/components/EmailLayout.svelte |
Adds shared email layout/header wrapper. |
src/Exceptionless.EmailTemplates/src/components/SocialFooter.svelte |
Adds shared social/contact footer. |
src/Exceptionless.EmailTemplates/src/helpers/raw.js |
Removes legacy Handlebars raw helper. |
src/Exceptionless.EmailTemplates/src/layouts/default.html |
Removes legacy default layout. |
src/Exceptionless.EmailTemplates/src/layouts/index-layout.html |
Removes legacy index layout. |
src/Exceptionless.EmailTemplates/src/pages/event-notice.html |
Removes legacy event notice source template. |
src/Exceptionless.EmailTemplates/src/pages/index.html |
Removes legacy preview index page. |
src/Exceptionless.EmailTemplates/src/pages/organization-added.html |
Removes legacy organization added source template. |
src/Exceptionless.EmailTemplates/src/pages/organization-invited.html |
Removes legacy organization invited source template. |
src/Exceptionless.EmailTemplates/src/pages/organization-notice.html |
Removes legacy organization notice source template. |
src/Exceptionless.EmailTemplates/src/pages/organization-payment-failed.html |
Removes legacy payment failed source template. |
src/Exceptionless.EmailTemplates/src/pages/project-daily-summary.html |
Removes legacy daily summary source template. |
src/Exceptionless.EmailTemplates/src/pages/user-email-verify.html |
Removes legacy email verify source template. |
src/Exceptionless.EmailTemplates/src/pages/user-password-reset.html |
Removes legacy password reset source template. |
src/Exceptionless.EmailTemplates/src/partials/social.html |
Removes legacy social partial. |
src/Exceptionless.EmailTemplates/src/stories/EmailPreview.svelte |
Adds iframe-based email preview component. |
src/Exceptionless.EmailTemplates/src/stories/event-notice.stories.svelte |
Adds Storybook story for event notice. |
src/Exceptionless.EmailTemplates/src/stories/organization-added.stories.svelte |
Adds Storybook story for organization added. |
src/Exceptionless.EmailTemplates/src/stories/organization-invited.stories.svelte |
Adds Storybook story for organization invited. |
src/Exceptionless.EmailTemplates/src/stories/organization-notice.stories.svelte |
Adds Storybook story for organization notice. |
src/Exceptionless.EmailTemplates/src/stories/organization-payment-failed.stories.svelte |
Adds Storybook story for payment failed. |
src/Exceptionless.EmailTemplates/src/stories/project-daily-summary.stories.svelte |
Adds Storybook story for daily summary. |
src/Exceptionless.EmailTemplates/src/stories/sample-data.ts |
Adds sample token evaluator/data for email previews. |
src/Exceptionless.EmailTemplates/src/stories/user-email-verify.stories.svelte |
Adds Storybook story for email verification. |
src/Exceptionless.EmailTemplates/src/stories/user-password-reset.stories.svelte |
Adds Storybook story for password reset. |
src/Exceptionless.EmailTemplates/src/templates/event-notice.svelte |
Adds Svelte source for event notice email. |
src/Exceptionless.EmailTemplates/src/templates/organization-added.svelte |
Adds Svelte source for organization added email. |
src/Exceptionless.EmailTemplates/src/templates/organization-invited.svelte |
Adds Svelte source for organization invitation email. |
src/Exceptionless.EmailTemplates/src/templates/organization-notice.svelte |
Adds Svelte source for organization notice email. |
src/Exceptionless.EmailTemplates/src/templates/organization-payment-failed.svelte |
Adds Svelte source for payment failed email. |
src/Exceptionless.EmailTemplates/src/templates/project-daily-summary.svelte |
Adds Svelte source for daily summary email. |
src/Exceptionless.EmailTemplates/src/templates/user-email-verify.svelte |
Adds Svelte source for email verification email. |
src/Exceptionless.EmailTemplates/src/templates/user-password-reset.svelte |
Adds Svelte source for password reset email. |
src/Exceptionless.EmailTemplates/src/theme.ts |
Adds centralized email color/theme tokens. |
src/Exceptionless.EmailTemplates/svelte.config.js |
Adds Svelte preprocessing config. |
src/Exceptionless.EmailTemplates/tsconfig.json |
Adds TypeScript config for the email-template project. |
src/Exceptionless.EmailTemplates/vite.config.ts |
Adds Vite SSR build config. |
src/Exceptionless.Web/ClientApp/package.json |
Prevents ClientApp Storybook from auto-opening a browser. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Dogfood Evidence — Mailpit Email RendersAll 8 templates sent to Mailpit via SMTP and visually verified. HTML Check scores range 87–94%. Bug Found & Fixed:
|
| Template | Result |
|---|---|
| Event Notice | ✅ Error details table, View Event Details CTA, User Info, structured data |
| Daily Summary | ✅ Stats grid (Count/Unique/New), View Timeline, Most Frequent with [REGRESSED] badge |
| Organization Invite | ✅ Join Organization CTA, Connect/Contact footer |
| Organization Added | ✅ View Organization CTA |
| Organization Notice (monthly) | ✅ Monthly plan limit message |
| Payment Failed | ✅ Update Billing Information CTA |
| Account Confirmation | ✅ Verify Address CTA, personalized greeting |
| Password Reset | ✅ Reset Password CTA |
📸 Visual Comparison: All 8 Templates (Before → After)All templates rendered in Mailpit with identical sample data. Left = main branch (Foundation/Inky), Right = PR (Svelte 5 + Tailwind). Email VerifyPassword ResetEvent NoticeDaily Summary
Organization AddedOrganization InvitedOrganization Notice (Throttled)Payment FailedVerification: All 16 emails (8 OLD + 8 NEW) successfully delivered to Mailpit SMTP. All 26 mailer tests pass. 🟢 |
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>
- vite.config.ts: add closeBundle plugin so 'dev' (--watch) regenerates
HTML templates on every source change; simplify 'build' script to
'vite build' (plugin handles node dist/build.js). Remove try/catch so
renderer failures propagate and fail the build.
- sample-data.ts: fix isTruthy to match Handlebars semantics exactly —
store @index as a real number so {{#if @index}} is falsy at index 0
(matches HandlebarsDotNet integer semantics); strings 'false'/'null'/
'undefined'/'' are falsy, all other strings truthy (matching
Handlebars.js, not JS). Change BASE_URL to http://localhost:7110.
- dependabot.yml: add npm entry for /src/Exceptionless.EmailTemplates
- build.yaml: add test-email-templates CI job that runs lint, check,
build, and verifies generated HTML is committed and up-to-date
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add IsRegressed to GetStackTemplateData in Mailer.cs
- Fix project-daily-summary: wrap all MostFrequent/Newest items in single <ul>
- Refactor SocialFooter to use Row+Column components (remove {#@html} hack)
- Use Preview component in EmailLayout; remove unconditional spacer div
- Replace 200-line custom Handlebars evaluator with handlebars npm package
- Move JSON-LD to wrapJsonLd() helper in src/lib/json-ld.ts (avoids Svelte
parser treating <script> in template literals as real script elements)
- Include src/templates/** in ESLint with no-at-html-tags rule disabled
- Remove .gstack/ from repo-root .gitignore
- Remove obvious code comments from build-emails.ts
- Fix no-unused-vars: remove Link import from user-email-verify.svelte
- Regenerate all 8 HTML templates
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Fields is iterated with {{#each Fields}} where @key = property name
and this = value. Using a plain object (not array of objects) matches
HandlebarsDotNet's Dictionary<string,string> iteration semantics and
produces correct key/value pairs in the Storybook preview.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ressed badge - RunMailJobAsync() now returns string body for assertions - All 25 tests assert template-specific content (buttons, headings, key phrases) - New test SendProjectDailySummaryWithRegressedStackAsync verifies [REGRESSED] badge appears when StackStatus.Regressed - this was the bug: IsRegressed was missing from GetStackTemplateData so the badge never rendered - Total: 26 tests passing Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…lpit Side-by-side comparisons of all 8 email templates rendered in Mailpit. BEFORE = main branch Foundation/Inky HTML, AFTER = PR Svelte 5 + Tailwind. Both rendered with identical sample data to verify pixel-accurate parity. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…daily summary Use @better-svelte-email/components Row and Column instead of raw HTML strings for the 3-column and 4-column stats tables in project-daily-summary.svelte. Also refactor the throttling text to use a single @html block instead of mixing @html with Svelte Link components. All 26 mailer tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
nodemailer was installed locally for email-to-Mailpit testing only. It's not needed as a production dependency. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
6a49a24 to
07dbfb8
Compare
# Conflicts: # src/Exceptionless.AppHost/Extensions/WorktreeScope.cs # src/Exceptionless.AppHost/Program.cs
|
Follow-up reviewer audit complete at I re-read all live review threads, inline comments, submitted reviews, and issue comments against the current branch-vs-main diff. The 18 review threads classify as 14 outdated/already-fixed or superseded-by-the-current implementation and 4 current/already-addressed. One previously resolved human finding had regressed: The thermo-nuclear pass found one additional real boundary gap: parity validation compared visible text and links but did not inspect Gmail Verification:
No remaining actionable review concern or external blocker was found. The complete structural pass found no >1,000-line source file, unsafe boundary/type leak, duplicated canonical helper, non-atomic update, or clear behavior-preserving broad refactor beyond the focused CI simplification above. |
|
Addressed the invitation feedback and audited every internal email URL against the Svelte UI. The organization invitation does not provision an account or password-reset token, so a setup-password page would be the wrong contract. A genuinely new address now follows the invitation email directly to I migrated the remaining legacy email routes to their Svelte equivalents and extended parity validation to discover SvelteKit routes and verify both clean URLs and the current Verification at
Dependencies were updated to the latest compatible releases; remaining outdated entries are major-version upgrades and were intentionally left for separate compatibility work. |
|
Replacement CI is fully green at |








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
@better-svelte-email/server+ Tailwind CSS (inlined)npm run buildinsrc/Exceptionless.EmailTemplatesrenders all 8 templates tosrc/Exceptionless.Core/Mail/Templates/*.htmlMailer.csnow emits a safely encodedmailto:token; HandlebarsDotNet rendering remains authoritative and is covered by hostile-input regression testsSecurity Fixes
{@html preheader}→{preheader}text bindingSecurity 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)
@ts-nocheckfrom components — proper TypeScript throughoutimport type { Snippet } from 'svelte'src/theme.ts)parseIntcalls to include radix argument{@html}inSocialFooterinto single blockcompilerOptions.generatefrom vite.config.ts (dropped in Svelte 5)Bug Fixes
1.
IsRegressedbadge silently broken (fixed in this PR)GetStackTemplateDatainMailer.csnever includedIsRegressedin the data dictionary, so{{#if IsRegressed}}[REGRESSED]{{/if}}in templates always evaluated false — the badge never appeared for regressed stacks. This PR addsIsRegressed = s.Status == StackStatus.Regressedto fix it.2. Zero body assertions in mailer tests (coverage gap fixed)
RunMailJobAsync()only logged the rendered body — zero assertions. Broken template rendering would pass silently. Fixed by:RunMailJobAsync()now returns the rendered HTML body<!DOCTYPE html, no unrendered{{tokensSendProjectDailySummaryWithRegressedStackAsynctest verifies[REGRESSED]appears for regressed stacks3. JSON-LD whitespace collapse bug
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
npm run storybook)src/theme.ts: centralized design tokens (primary, dark, alert, muted colors)build,dev,check,lint,format,storybook,build-storybookVisual Parity
All 8 templates verified via Mailpit SMTP delivery + browser screenshots. HTML Check scores: 87–94%.
See PR comment below for Mailpit screenshots of all 8 rendered emails.
How to Run
Tests
All 26 mailer tests pass (25 existing + 1 new
IsRegressedtest):Breaking Changes
None. C#
Mailer.csand all Handlebars templates are fully backward-compatible. The compiled.htmloutput format is identical to the old toolchain.Final production-readiness verification (2026-07-12)
origin/mainwith merge commitb25c77ac2; hardening commit3700bbfac.mailto:query injection, Outlook conditional-comment stripping, stale/non-deterministic generated artifacts, Storybook production-template side effects, worktree port collisions, mobile overflow, and missing client reset/title metadata.npm auditreports 0 vulnerabilities.