Skip to content

Replace MailerSend template sprawl with a repo-owned render endpoint - #1065

Draft
hturnbull93 wants to merge 7 commits into
PauseAI:mainfrom
hturnbull93:feat/onboarding-email-render-endpoint
Draft

Replace MailerSend template sprawl with a repo-owned render endpoint#1065
hturnbull93 wants to merge 7 commits into
PauseAI:mainfrom
hturnbull93:feat/onboarding-email-render-endpoint

Conversation

@hturnbull93

Copy link
Copy Markdown
Collaborator

Summary

Replaces the 7 hand-authored MailerSend templates driven by airtable-mailersend-emails.js with a repo-owned render endpoint, matrixed on intent × chapter × language instead of one template ID per combination. Chapter content (leader, links) now comes live from the same Airtable "National Groups" data /api/national-groups already uses, instead of being hand-copied per template and drifting out of date.

  • src/lib/server/onboardingEmail/ — chapter lookup, intent copy (act-now / keep-informed / volunteer / lead), language handling (en/es/fr), HTML + plain-text renderers
  • src/routes/api/onboarding-email — bearer-secret POST endpoint, returns {subject, html, text}. Render-only — never sends mail itself, never touches the MailerSend API key.
  • src/routes/api/unsubscribe — new. No unsubscribe mechanism existed in any of the 7 templates before this (MailerSend's unsubscribe_rate: 0 reflected "no link offered," not "no one wants out"). Flips the Members Email subscription checkbox and suppresses on both MailerSend domains.
  • src/routes/onboarding-email-preview — dev-only QA page (404s outside dev), renders a sample of intent × chapter × language combos.
  • airtable-mailersend-emails.js — tries the new endpoint first; on any failure (network error, timeout, non-OK response, missing fields) falls through to the original template_id logic, byte-for-byte unchanged. Worst case is today's behavior.
  • email-templates/ — raw MailerSend payloads for all 7 old templates, kept as migration reference and to document routing gaps found along the way (CANADA_FR_TEMPLATE was unreachable in practice — languages is always empty at send time; Lead intent normally hands off via a mailto: link rather than a form submission, per docs/join-form-flow.md).

Please manually check / review / adjust

  • Spanish copy is newly written, not adapted from anything reviewed — no es locale exists elsewhere in the codebase, and the old Spanish template only ever covered the volunteer case. Needs a native/fluent Spanish speaker from the org to read it.
  • French register: the join form's French (messages.ts) uses informal tu; the old Canada-FR MailerSend template used formal vous. Matched vous to stay consistent with the reviewed body copy — meaning the email doesn't literally echo the join form's wording. Confirm that's the right call.
  • Subject line no longer includes the chapter name (previously e.g. "Welcome to PauseAI UK {{first_name}}!") — check /onboarding-email-preview and confirm the new subject is acceptable.
  • Visual simplification: no logo/wordmark image, social/community links render as plain text rather than icons (deliberate — avoids broken-image risk in email clients, but is a visual downgrade from the old templates). Confirm OK for v1.
  • Bluesky link is hardcoded (confirmed global across every old template, including UK's — no per-chapter Bluesky field exists on NationalGroup/Airtable).
  • Actually load /onboarding-email-preview locally and look at real rendered output — only agent/automated smoke-testing has happened so far, no human has eyeballed it.

Pre-merge / pre-launch checklist

  • Set real values for ONBOARDING_EMAIL_RENDER_SECRET and MAILERSEND_API_KEY in prod (Netlify env vars) — both currently placeholder-only in .env (gitignored, no real values committed).
  • Map the same shared secret (name it exactly Onboarding email render secret) and the optional render_endpoint_url override in the Airtable automation's script input config panel — the script references these by name but Airtable-side mapping isn't part of this repo.
  • Confirm with MailerSend support whether raw (non-template_id) sends get any automatic unsubscribe-footer/compliance injection. Docs suggest that's template-engine-only, so the shell here builds its own unsubscribe link + physical address (Box C5957, Kwikstaartlaan 42, 3704GS Zeist, The Netherlands) — never got a definitive answer from MailerSend, worth chasing before this goes live.
  • Trigger one real test send through the Airtable automation once deployed, to confirm the full path end-to-end (not just the isolated endpoint checks done so far).
  • Once confident in the new path, consider whether/when to retire the 7 old MailerSend templates (kept live for now as the fallback).

🤖 Generated with Claude Code

https://claude.ai/code/session_01GtQwhn5Fb9eiC4LsmRs47q

Airtable's onboarding automation sent every welcome email via one of 7
hand-authored MailerSend templates, hardcoding chapter links/leaders that
already drift from the live National Groups data, and only distinguishing
volunteer vs non-volunteer recipients. Adds a render endpoint that composes
subject/html/text from live chapter data, the recipient's actual intent
(act-now/keep-informed/volunteer/lead), and language - so new chapters and
languages no longer need a new MailerSend template + script branch.

- src/lib/server/onboardingEmail/ - chapter lookup (reuses the National
  Groups fetch), intent/language copy, HTML + plain-text renderers
- src/routes/api/onboarding-email - bearer-secret POST endpoint returning
  {subject, html, text}; render-only, never sends mail itself
- src/routes/api/unsubscribe - suppresses on both MailerSend domains and
  flips the Members "Email subscription" checkbox; no unsubscribe
  mechanism existed in any of the 7 templates before this
- src/routes/onboarding-email-preview - dev-only QA page, 404s outside dev
- airtable-mailersend-emails.js - tries the new endpoint first, falls
  through to the untouched template_id logic on any failure
- email-templates/ - raw MailerSend payloads for all 7 templates, kept as
  migration reference and to document the old routing gaps (Canada-FR was
  unreachable in practice; Lead hands off via mailto instead of a form
  submission)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GtQwhn5Fb9eiC4LsmRs47q
@netlify

netlify Bot commented Aug 25, 2026

Copy link
Copy Markdown

Deploy Preview for pauseai ready!

Name Link
🔨 Latest commit 483a8d4
🔍 Latest deploy log https://app.netlify.com/projects/pauseai/deploys/6a9870c1f1267e0008a10731
😎 Deploy Preview https://deploy-preview-1065--pauseai.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
Lighthouse
Lighthouse
1 paths audited
Performance: 60 (🟢 up 7 from production)
Accessibility: 91 (no change from production)
Best Practices: 100 (no change from production)
SEO: 82 (no change from production)
PWA: -
View the detailed breakdown and full score reports
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@hturnbull93
hturnbull93 marked this pull request as ready for review September 2, 2026 17:54
hturnbull93 and others added 3 commits September 2, 2026 19:02
svelte-check generates $env/static/private types from .env, which CI
populates via `cp template.env .env`. The static imports added in this
branch (MAILERSEND_API_KEY in the unsubscribe route,
ONBOARDING_EMAIL_RENDER_SECRET in the onboarding-email route) referenced
members missing from template.env, failing `pnpm check` and blocking the
deploy preview. Values left empty; Netlify secret scanning rejects real
values in committed files.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019P2Yg2vt6iV6w1rAL1wS4h
Two failures blocking the deploy preview for PauseAI#1065:

- Netlify's build does not populate .env from template.env; it exposes
  only the vars configured in the Netlify environment. That list has
  MAILERSEND_API_KEY but not ONBOARDING_EMAIL_RENDER_SECRET, and
  $env/static/private fails the build outright on a missing member.
  Switch the onboarding-email route to $env/dynamic/private (runtime
  lookup, no build-time export requirement), matching the pattern in
  contact-us/+page.server.ts.

- eslint's typed linting ran on airtable-mailersend-emails.js, which is
  an Airtable script-runtime file not covered by tsconfig.check.json,
  producing a parser error. Add it to eslint's globalIgnores.

pnpm check and pnpm build both pass locally.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019P2Yg2vt6iV6w1rAL1wS4h
Gate the QA route on hostname rather than dev only: dev, localhost, and
*.netlify.app can load it; the production domain still 404s. Lets
reviewers eyeball the rendered emails on the PR's deploy preview without
running the site locally. The page renders public National Groups data
and a fake recipient, no PII, so preview URLs are an acceptable exposure.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019P2Yg2vt6iV6w1rAL1wS4h
@hturnbull93
hturnbull93 marked this pull request as draft September 2, 2026 18:23
hturnbull93 and others added 2 commits September 2, 2026 19:42
Replace the fixed list of 10 preset intent x chapter x language combos
with a form exposing each renderer input on its own: first name (text),
country (free-text with a datalist of the 196 known names), intent
(select, including the empty/unrecognised case), and languages
(checkboxes over the stored vocabulary). Values live in the query string
so a preview is shareable, and any change auto-submits the GET form
(plain submit button as the no-JS fallback).

Adds a "resolved" panel showing which branch the inputs hit: email
language, intent bucket, and whether the country matched a National
Groups chapter (with leader and link count) or fell back to Global.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019P2Yg2vt6iV6w1rAL1wS4h
…scheme

- Add an optional languageOverride to OnboardingEmailParams so the preview
  can force en/es/fr directly instead of reverse-engineering country +
  languages. The production render endpoint never sets it.
- Preview language control is now three radios (en/es/fr) — the only
  languages copy.ts actually has copy for.
- Country is a <select> populated from the active National Groups records
  (new listActiveChapterCountries() helper, same base/table/filter as the
  chapter lookup), plus a "global fallback" option. These are the only
  countries that resolve to a real chapter block.
- Pin the page to a light color-scheme with explicit background/colour on
  the wrapper and the two light-background panels; the site's dark theme
  was leaving light text on them (white-on-white).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019P2Yg2vt6iV6w1rAL1wS4h
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