feat: modernize email templates with Svelte 5 + Tailwind + Storybook#2258
feat: modernize email templates with Svelte 5 + Tailwind + Storybook#2258niemyjski wants to merge 11 commits into
Conversation
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>
✅ 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.
…ates-modernization
- 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>
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 |
…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>
📸 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. 🟢 |
…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>








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.csandHandlebarsDotNetrendering pipeline untouchedSecurity 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.