MJML email service: branded templates + thin sender integration #134

Closed
opened 2026-03-21 16:46:20 +00:00 by forgejo_admin · 0 comments

Type

Feature

Lineage

plan-wkq → Phase 11 (Girls Tryout) → discovered scope → new capability
Triggered by jersey email images fix (MinIO CDN). Existing 825-line email.py has hardcoded HTML in Python f-strings — not enterprise.

Repo

forgejo_admin/westside-emails (new repo) + forgejo_admin/basketball-api (integration)

User Story

As an admin, I want to send branded email announcements so that parent comms are professional and consistent
As an admin, I want to edit email content and preview before sending so that I don't need a developer for copy changes
As a parent, I want to receive a branded email with jersey ordering link so that I can order without bringing cash
As a parent, I want to receive clear email communications with action links so that I never miss a deadline

Context

All four user stories are documented on the Westside project page under User Roles & Stories. Current email system embeds 825 lines of inline-CSS HTML inside Python f-strings in services/email.py. Every copy change (dates, locations, pricing) requires a code change, PR, deploy. No preview capability. Brand styles duplicated across 4 email functions. Not reusable across projects.

MJML is the industry-standard enterprise solution for email templating. Write semantic markup (mj-section, mj-button), compile to battle-tested inline-CSS HTML that renders in Gmail, Outlook, Apple Mail. Templates live in their own repo, decoupled from API code.

File Targets

New repo: westside-emails

  • src/base-layout.mjml — shared brand wrapper (header, footer, colors)
  • src/jersey-reminder.mjml — first template: girls jersey ordering reminder
  • src/tryout-announcement.mjml — port existing announcement email
  • src/confirmation.mjml — port existing registration confirmation
  • src/roster-export.mjml — port existing roster email
  • sample-data/jersey-reminder.json — sample data for preview
  • package.json — mjml CLI dependency
  • preview/ — local preview server

basketball-api changes:

  • src/basketball_api/services/email.py — replace f-string HTML with template loader + string replace + send
  • src/basketball_api/routes/admin.py — update email endpoints to use templates

Files the agent should NOT touch:

  • src/basketball_api/routes/jersey.py — Stripe checkout logic, unrelated
  • src/basketball_api/routes/webhooks.py — Stripe webhook handler, unrelated

Acceptance Criteria

  • When I run npm run build in westside-emails, MJML compiles to HTML in dist/
  • When I open dist/jersey-reminder.html in a browser, it renders the branded email with placeholder markers
  • When basketball-api sends the jersey reminder, it loads compiled HTML and replaces {{name}}, {{jersey_url}} etc.
  • When I receive the test email in Gmail, images render (MinIO CDN), layout is correct on desktop and mobile
  • When I edit copy in the .mjml file, I can preview changes without touching Python code
  • When postgres_wal or tf_state_backups are accessed anonymously, they return 403 (CDN only for assets)

Test Expectations

  • MJML compilation: npx mjml src/*.mjml -o dist/ exits 0 with valid HTML output
  • Template rendering: Python test that loads compiled HTML, replaces variables, asserts output contains expected values
  • Email send: test email to devopsphilosopher@gmail.com renders correctly in Gmail
  • Run command: npm run build && npm run preview for visual check

Constraints

  • MJML for compilation, NOT Jinja2 or any server-side template engine
  • {{variable}} markers are literal strings in compiled HTML — basketball-api does simple string replacement at send time
  • Gmail OAuth (westsidebasketball@gmail.com) for sending — no SendGrid, no SaaS
  • Brand colors/styles must match existing westside-app design system
  • First template is girls jersey reminder — gentle tone, no pressure, opt-out friendly

Checklist

  • westside-emails repo created on Forgejo
  • base-layout.mjml with Westside brand
  • jersey-reminder.mjml template
  • basketball-api integration (template loader replaces f-string HTML)
  • Test email sent and verified in Gmail
  • PR opened for basketball-api changes
  • No unrelated changes
  • project-westside-basketball — project page with user stories this fulfills
  • plan-wkq — parent plan
  • Issue #126 (pal-e-platform) — MinIO CDN that enables public image URLs in emails
### Type Feature ### Lineage `plan-wkq` → Phase 11 (Girls Tryout) → discovered scope → new capability Triggered by jersey email images fix (MinIO CDN). Existing 825-line email.py has hardcoded HTML in Python f-strings — not enterprise. ### Repo `forgejo_admin/westside-emails` (new repo) + `forgejo_admin/basketball-api` (integration) ### User Story As an admin, I want to send branded email announcements so that parent comms are professional and consistent As an admin, I want to edit email content and preview before sending so that I don't need a developer for copy changes As a parent, I want to receive a branded email with jersey ordering link so that I can order without bringing cash As a parent, I want to receive clear email communications with action links so that I never miss a deadline ### Context All four user stories are documented on the Westside project page under User Roles & Stories. Current email system embeds 825 lines of inline-CSS HTML inside Python f-strings in `services/email.py`. Every copy change (dates, locations, pricing) requires a code change, PR, deploy. No preview capability. Brand styles duplicated across 4 email functions. Not reusable across projects. MJML is the industry-standard enterprise solution for email templating. Write semantic markup (`mj-section`, `mj-button`), compile to battle-tested inline-CSS HTML that renders in Gmail, Outlook, Apple Mail. Templates live in their own repo, decoupled from API code. ### File Targets **New repo: `westside-emails`** - `src/base-layout.mjml` — shared brand wrapper (header, footer, colors) - `src/jersey-reminder.mjml` — first template: girls jersey ordering reminder - `src/tryout-announcement.mjml` — port existing announcement email - `src/confirmation.mjml` — port existing registration confirmation - `src/roster-export.mjml` — port existing roster email - `sample-data/jersey-reminder.json` — sample data for preview - `package.json` — mjml CLI dependency - `preview/` — local preview server **basketball-api changes:** - `src/basketball_api/services/email.py` — replace f-string HTML with template loader + string replace + send - `src/basketball_api/routes/admin.py` — update email endpoints to use templates Files the agent should NOT touch: - `src/basketball_api/routes/jersey.py` — Stripe checkout logic, unrelated - `src/basketball_api/routes/webhooks.py` — Stripe webhook handler, unrelated ### Acceptance Criteria - [ ] When I run `npm run build` in westside-emails, MJML compiles to HTML in `dist/` - [ ] When I open `dist/jersey-reminder.html` in a browser, it renders the branded email with placeholder markers - [ ] When basketball-api sends the jersey reminder, it loads compiled HTML and replaces `{{name}}`, `{{jersey_url}}` etc. - [ ] When I receive the test email in Gmail, images render (MinIO CDN), layout is correct on desktop and mobile - [ ] When I edit copy in the `.mjml` file, I can preview changes without touching Python code - [ ] When postgres_wal or tf_state_backups are accessed anonymously, they return 403 (CDN only for assets) ### Test Expectations - [ ] MJML compilation: `npx mjml src/*.mjml -o dist/` exits 0 with valid HTML output - [ ] Template rendering: Python test that loads compiled HTML, replaces variables, asserts output contains expected values - [ ] Email send: test email to devopsphilosopher@gmail.com renders correctly in Gmail - Run command: `npm run build && npm run preview` for visual check ### Constraints - MJML for compilation, NOT Jinja2 or any server-side template engine - `{{variable}}` markers are literal strings in compiled HTML — basketball-api does simple string replacement at send time - Gmail OAuth (westsidebasketball@gmail.com) for sending — no SendGrid, no SaaS - Brand colors/styles must match existing westside-app design system - First template is girls jersey reminder — gentle tone, no pressure, opt-out friendly ### Checklist - [ ] westside-emails repo created on Forgejo - [ ] base-layout.mjml with Westside brand - [ ] jersey-reminder.mjml template - [ ] basketball-api integration (template loader replaces f-string HTML) - [ ] Test email sent and verified in Gmail - [ ] PR opened for basketball-api changes - [ ] No unrelated changes ### Related - `project-westside-basketball` — project page with user stories this fulfills - `plan-wkq` — parent plan - Issue #126 (pal-e-platform) — MinIO CDN that enables public image URLs in emails
forgejo_admin 2026-03-21 17:08:50 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
forgejo_admin/basketball-api#134
No description provided.