Repository navigation
feat(email): one shared layout for every Studio email - #463
Merged
Merged
Conversation
Every email the app sends (managed-auth magic link and invite, workspace invites, billing, trial reminder, usage alerts, form and comment notifications, webhook alerts) and the Supabase auth templates now render through one layout in server/utils/email-layout.ts: 600px table layout, hosted logo at /email/logo.png, heading, lead, facts, quote, button, notes, sign-off and footer, with a plain-text part for each. - Copy stays in the email-templates collection; the model is now structured (heading, lead, facts, quote, cta, notes) instead of raw HTML. Layout labels live in ui-strings (email.*). - All params are escaped at render time; copy takes only bold, code and link markup, so a user value can never inject HTML or a link. - Reply-To is set from emailReplyTo, so the 'reply to this email' lines are true. - supabase/templates/*.html are generated from the same layout and a test fails when they drift. - pnpm email:previews writes every email with sample data.
Member
Author
|
t3 read @b843b05e: OK. The notes below are non-blocking.
Checks (detached worktree at b843b05, after
The one integration red is the billing-webhook "v2 product" test. It is date-dependent, fails identically on untouched main 2438d9f, and is fixed separately in #464 (test-only). |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Every email Studio sends now goes through one layout (
server/utils/email-layout.ts,renderEmail(parts, chrome, env) -> { html, text }), matching the approved payment-received reference: 600px light-only table layout, hosted logo, heading, lead, facts, quote, button, notes, sign-off, footer. Each email also has a plain-text part.public/email/logo.png, loaded from<siteUrl>/email/logo.png(data URIs are blocked by Gmail/Outlook). Width/height/alt set. No hero images (none invented).email-templatesmodel is now structured (heading,lead,facts,quote,cta_label,cta_url,notes,about,preheader) instead of raw HTML; layout labels (sign-off, footer) areemail.*keys inui-strings. Note: the content files were edited directly, not through the MCP write tools (not available in this session);contentrain-query generateruns clean on them.{params}are escaped at render time; the only markup in copy is**bold**,`code`,[label]({urlParam}). A user value is never read as markup or as a link; buttons only render for absolute http(s) URLs; subjects have line breaks stripped. The two callers that pre-escaped (comments, forms) now pass raw values.usage_alert.resets_onlost its<strong>tags for the same reason.emailReplyTo(NUXT_EMAIL_REPLY_TO, defaultsupport@contentrain.io, the same address migrate uses).supabase/templates/*.htmlare generated from the same layout (supabase-*slugs,{{ .SiteURL }}/{{ .ConfirmationURL }}); a test fails when a committed file drifts (pnpm email:supabaserewrites them). Invite keeps its 24 h expiry line (Supabase's own).pnpm email:previewswrites every email with sample data to.tmp/email-previews.Needs founder
support@contentrain.iois a monitored inbox, or setNUXT_EMAIL_REPLY_TOon Railway (not touched here).https://github.com/Contentrain) and privacy (https://contentrain.io/legal/privacy-policy) URLs are inferred from iterum; confirm. LinkedIn / X links of the reference are not in the footer.Sender inventory
studio — transport:
useEmailProvider()-> Resend (server/providers/resend-email.ts); copy:email-templatescollection viaemailTemplate(slug, params)(server/utils/content-strings.ts)auth-magic-link)server/providers/managed-auth.ts:547-548auth-invite)server/providers/managed-auth.ts:568-569supabase/templates/{magic-link,invite,confirmation}.html+supabase/config.toml(now generated from the same layout; slugssupabase-*)invite-added)server/utils/db.ts:209-222invite-reminder)server/api/workspaces/[workspaceId]/members/[memberId]/resend.post.ts:61-70trial-ending)server/plugins/trial-reminder.ts:108-121subscription-activated)server/api/billing/webhook/[provider].post.ts(sendBillingEmail, :182-230; callers :85, :431, :669, :700)payment-recovered):90)payment-failed):557)subscription-cancel-scheduled):568)subscription-canceled):606)usage-warning)server/utils/usage-alerts.ts:190-213viaserver/plugins/usage-alerts.ts:38usage-limit-reached)usage-overage-started)form-submitted)server/utils/form-types.ts:116-138comment-pending,comment-published)server/utils/comment-notifications.ts:24-44webhook-dlq-alert)ee/enterprise/webhook-dispatch.ts:214-253Not senders: account deletion (typed-email confirmation in the UI, no email, per CLAUDE.md); Polar/Stripe receipts (the payment provider's own, outside the repo).
Gates
pnpm lint: 0 errors (7 pre-existing warnings)pnpm typecheck: cleanpnpm test:unit: 202 files passed, 1 skipped; 2323 tests passed, 2 skippedpnpm test:integration: 53 files passed, 1 failed; 516 passed, 1 failed. The failure,billing-webhook ... a move onto a v2 product changes the unit through the converting call, is date-dependent (expects period key2026-09-15, gets2026-10-15) and unrelated to email. Same on untouched main.pnpm test:nuxt: 52 files, 358 tests passednode scripts/check-dictionary-keys.mjs: all literal keys defined