MJML email system: brand base + three layout templates + docker build compile #293

Closed
opened 2026-04-03 18:38:14 +00:00 by forgejo_admin · 4 comments

Type

Feature

Lineage

Standalone — discovered during email architecture review (2026-04-03). Supersedes board items #658 (audit contract offer email into MJML) and partially #735 (contract reminder branding).

Repo

forgejo_admin/basketball-api

User Story

As an admin,
I want email templates authored in MJML and compiled into the Docker image,
So that new emails are branded consistently without writing inline HTML in Python.

Context

email.py is 1,424 lines with 9 send functions. All use inline HTML f-strings except send_jersey_reminder_email() which uses load_email_template() to read compiled HTML from /data/email-templates/. The compiled HTML currently lives in an untracked k8s ConfigMap — no MJML source exists anywhere.

Decision: all emails will use three MJML layouts (notification, action, announcement) with {{key}} placeholders. load_email_template() already does the replacement — we just need the templates to exist and compile during build.

Email HTML constraints: no CSS Grid, no Flexbox, no CSS custom properties, no <style> blocks (Gmail strips them). MJML handles this by compiling to table-based inline-styled HTML. Mobile-first by default via <mj-column> stacking.

Brand tokens from brand.py: red #d42026, black #0a0a0a, dark #141414, border #262626, white #ffffff, font stack from FONT_FAMILY.

Architecture reference: arch-email in pal-e-docs.

Deployment Coordination

When this image deploys, do NOT set BASKETBALL_EMAIL_TEMPLATES_DIR env var — let the new config.py default (/app/templates/email/compiled/) take effect. The existing ConfigMap mount at /data/email-templates/ will shadow the baked-in path if it's still present, but the new default points to the baked-in location. ConfigMap removal is tracked in pal-e-deployments#83 (board item #753).

File Targets

Files to create:

  • templates/email/brand.mjml — shared <mj-attributes> with Westside tokens
  • templates/email/notification.mjml — "X happened" layout (headline + body + footer)
  • templates/email/action.mjml — "Do this thing" layout (headline + body + CTA button + footer)
  • templates/email/announcement.mjml — rich multi-section layout (headline + sections + optional CTA + footer)
  • templates/email/compiled/ — gitignored output directory
  • package.json — mjml dev dependency + build:email script
  • templates/email/jersey-reminder.mjml — recreate MJML source from current compiled HTML (migration proof)

Files to modify:

  • Dockerfile — add npm install + compile step, COPY compiled HTML to /app/templates/email/compiled/
  • src/basketball_api/config.py — change email_templates_dir default to /app/templates/email/compiled/

Files NOT to touch:

  • src/basketball_api/services/email.py — no code changes in this ticket, only templates and build
  • src/basketball_api/brand.py — tokens stay, MJML references same values

Acceptance Criteria

  • Three MJML layout files exist with {{key}} placeholders for headline, body, cta_text, cta_url, footer_note
  • npm run build:email compiles all MJML files to templates/email/compiled/*.html
  • Compiled HTML renders Westside-branded: red header border, dark card, correct font stack
  • Compiled action layout has a red CTA button that works on mobile
  • Dockerfile builds successfully with compiled templates at /app/templates/email/compiled/
  • load_email_template("action", {"headline": "Test", "body": "Hello", "cta_text": "Click", "cta_url": "https://example.com"}) returns valid HTML
  • jersey-reminder.mjml compiles to output visually matching current template (retrieve current from k8s: kubectl get configmap email-templates -n basketball-api -o jsonpath='{.data.jersey-reminder\.html}')

Test Expectations

  • Unit test: load_email_template() loads each compiled layout and replaces placeholders
  • Unit test: compiled HTML contains no {{key}} after replacement with complete data dict
  • Unit test: compiled HTML contains Westside brand colors (spot check #d42026, #0a0a0a)
  • Run command: pytest tests/test_jersey_reminder.py -v

Constraints

  • MJML v5 (latest stable)
  • No Jinja2 or template engine — keep {{key}} string replacement for simplicity
  • <mj-attributes> for shared styles, not <mj-style> (inline is safer for Gmail)
  • 600px max-width (email standard)
  • All layouts must be mobile-first (MJML default)
  • .gitignore the compiled/ directory — build artifact, not source

Checklist

  • PR opened
  • Tests pass
  • No unrelated changes
  • docker build succeeds locally
  • project-westside-basketball — westside project
  • arch-email — architecture reference
  • Board items #658, #735 — superseded by this ticket
  • pal-e-deployments#83 (board item #753) — downstream ConfigMap removal
### Type Feature ### Lineage Standalone — discovered during email architecture review (2026-04-03). Supersedes board items #658 (audit contract offer email into MJML) and partially #735 (contract reminder branding). ### Repo `forgejo_admin/basketball-api` ### User Story As an admin, I want email templates authored in MJML and compiled into the Docker image, So that new emails are branded consistently without writing inline HTML in Python. ### Context email.py is 1,424 lines with 9 send functions. All use inline HTML f-strings except `send_jersey_reminder_email()` which uses `load_email_template()` to read compiled HTML from `/data/email-templates/`. The compiled HTML currently lives in an untracked k8s ConfigMap — no MJML source exists anywhere. Decision: all emails will use three MJML layouts (notification, action, announcement) with `{{key}}` placeholders. `load_email_template()` already does the replacement — we just need the templates to exist and compile during build. Email HTML constraints: no CSS Grid, no Flexbox, no CSS custom properties, no `<style>` blocks (Gmail strips them). MJML handles this by compiling to table-based inline-styled HTML. Mobile-first by default via `<mj-column>` stacking. Brand tokens from `brand.py`: red #d42026, black #0a0a0a, dark #141414, border #262626, white #ffffff, font stack from FONT_FAMILY. Architecture reference: `arch-email` in pal-e-docs. ### Deployment Coordination When this image deploys, do NOT set `BASKETBALL_EMAIL_TEMPLATES_DIR` env var — let the new config.py default (`/app/templates/email/compiled/`) take effect. The existing ConfigMap mount at `/data/email-templates/` will shadow the baked-in path if it's still present, but the new default points to the baked-in location. ConfigMap removal is tracked in pal-e-deployments#83 (board item #753). ### File Targets Files to create: - `templates/email/brand.mjml` — shared `<mj-attributes>` with Westside tokens - `templates/email/notification.mjml` — "X happened" layout (headline + body + footer) - `templates/email/action.mjml` — "Do this thing" layout (headline + body + CTA button + footer) - `templates/email/announcement.mjml` — rich multi-section layout (headline + sections + optional CTA + footer) - `templates/email/compiled/` — gitignored output directory - `package.json` — mjml dev dependency + `build:email` script - `templates/email/jersey-reminder.mjml` — recreate MJML source from current compiled HTML (migration proof) Files to modify: - `Dockerfile` — add npm install + compile step, COPY compiled HTML to `/app/templates/email/compiled/` - `src/basketball_api/config.py` — change `email_templates_dir` default to `/app/templates/email/compiled/` Files NOT to touch: - `src/basketball_api/services/email.py` — no code changes in this ticket, only templates and build - `src/basketball_api/brand.py` — tokens stay, MJML references same values ### Acceptance Criteria - [ ] Three MJML layout files exist with `{{key}}` placeholders for headline, body, cta_text, cta_url, footer_note - [ ] `npm run build:email` compiles all MJML files to `templates/email/compiled/*.html` - [ ] Compiled HTML renders Westside-branded: red header border, dark card, correct font stack - [ ] Compiled action layout has a red CTA button that works on mobile - [ ] Dockerfile builds successfully with compiled templates at `/app/templates/email/compiled/` - [ ] `load_email_template("action", {"headline": "Test", "body": "Hello", "cta_text": "Click", "cta_url": "https://example.com"})` returns valid HTML - [ ] `jersey-reminder.mjml` compiles to output visually matching current template (retrieve current from k8s: `kubectl get configmap email-templates -n basketball-api -o jsonpath='{.data.jersey-reminder\.html}'`) ### Test Expectations - [ ] Unit test: `load_email_template()` loads each compiled layout and replaces placeholders - [ ] Unit test: compiled HTML contains no `{{key}}` after replacement with complete data dict - [ ] Unit test: compiled HTML contains Westside brand colors (spot check #d42026, #0a0a0a) - Run command: `pytest tests/test_jersey_reminder.py -v` ### Constraints - MJML v5 (latest stable) - No Jinja2 or template engine — keep `{{key}}` string replacement for simplicity - `<mj-attributes>` for shared styles, not `<mj-style>` (inline is safer for Gmail) - 600px max-width (email standard) - All layouts must be mobile-first (MJML default) - `.gitignore` the `compiled/` directory — build artifact, not source ### Checklist - [ ] PR opened - [ ] Tests pass - [ ] No unrelated changes - [ ] `docker build` succeeds locally ### Related - `project-westside-basketball` — westside project - `arch-email` — architecture reference - Board items #658, #735 — superseded by this ticket - pal-e-deployments#83 (board item #753) — downstream ConfigMap removal
Author
Owner

Scope Review: NEEDS_REFINEMENT

Review note: review-750-2026-04-03

Well-scoped feature ticket with complete template sections, verified file targets, and correct repo placement. Three fixable issues:

  • [SCOPE] Missing architecture note arch-email in pal-e-docs — board item has arch:email label but no backing note exists.
  • [BODY] Send function count says "11" but email.py has 9. Minor inaccuracy.
  • [BODY] Add deployment coordination note: when deployed, BASKETBALL_EMAIL_TEMPLATES_DIR env var must NOT be set so the new default /app/templates/email/compiled/ takes effect. ConfigMap removal tracked in #753.
  • [BODY] AC7 (jersey-reminder parity): specify where the agent can find the current compiled jersey-reminder.html for reference — it's in an untracked k8s ConfigMap, not in any repo.
## Scope Review: NEEDS_REFINEMENT Review note: `review-750-2026-04-03` Well-scoped feature ticket with complete template sections, verified file targets, and correct repo placement. Three fixable issues: - **[SCOPE]** Missing architecture note `arch-email` in pal-e-docs — board item has `arch:email` label but no backing note exists. - **[BODY]** Send function count says "11" but email.py has 9. Minor inaccuracy. - **[BODY]** Add deployment coordination note: when deployed, `BASKETBALL_EMAIL_TEMPLATES_DIR` env var must NOT be set so the new default `/app/templates/email/compiled/` takes effect. ConfigMap removal tracked in #753. - **[BODY]** AC7 (jersey-reminder parity): specify where the agent can find the current compiled `jersey-reminder.html` for reference — it's in an untracked k8s ConfigMap, not in any repo.
Author
Owner

Scope Review (re-review): NEEDS_REFINEMENT

Review note: review-750-2026-04-03-r2 (child of review-750-2026-04-03)

Three of four items from the previous review are fixed. One remains:

  • [SCOPE] arch-email architecture note does not exist in pal-e-docs. search_notes("arch-email") returns empty. The issue body references it, the board item has the arch:email label, but the backing note was not created. Create arch-email note, then this ticket is READY.
## Scope Review (re-review): NEEDS_REFINEMENT Review note: `review-750-2026-04-03-r2` (child of `review-750-2026-04-03`) Three of four items from the previous review are fixed. One remains: - **[SCOPE]** `arch-email` architecture note does not exist in pal-e-docs. `search_notes("arch-email")` returns empty. The issue body references it, the board item has the `arch:email` label, but the backing note was not created. Create `arch-email` note, then this ticket is READY.
Author
Owner

Scope Review: APPROVED

Review note: review-750-2026-04-03-r3

Re-review round 3. The sole remaining blocker from R2 — arch-email note missing — is now resolved. get_note(slug="arch-email") returns note ID 1102 with full architecture content. All four R2 items verified fixed. Template completeness 12/12, traceability 5/5, all 11 file targets verified, dependencies correctly sequenced, 7 ACs all agent-verifiable. Ticket is ready for dispatch.

## Scope Review: APPROVED Review note: `review-750-2026-04-03-r3` Re-review round 3. The sole remaining blocker from R2 — arch-email note missing — is now resolved. `get_note(slug="arch-email")` returns note ID 1102 with full architecture content. All four R2 items verified fixed. Template completeness 12/12, traceability 5/5, all 11 file targets verified, dependencies correctly sequenced, 7 ACs all agent-verifiable. Ticket is ready for dispatch.
forgejo_admin 2026-04-03 19:13:47 +00:00
Author
Owner

Validation: FAIL

Tiers executed: Tier 1 (local tests), Tier 3 (prod pod/health check)
Validation note: validation-293-2026-04-03

13 checks: 10 PASS, 3 FAIL

Failures:

  • Dockerfile build (blocker): Line 11 runs npx mjml 'templates/email/*.mjml' -o templates/email/compiled/ but the compiled/ directory does not exist in the Docker build stage. MJML CLI errors: "Multiple input files, but output option should be either an existing directory or an empty string." Fix: add RUN mkdir -p templates/email/compiled before the compile step.
  • CI pipeline #288: build-and-push step failed (mkdir bug above). test step also failed with 9 pre-existing opt_out test failures (from #263, not this PR). update-kustomize-tag skipped.
  • Image not deployed: Pod still running old image 23902fe5.... Merge commit cf37b1b was never built/pushed.

No production regression — existing pod is healthy (0 restarts, /healthz 200). New code never deployed.

Discovered scope:

  1. Dockerfile mkdir fix (blocker for this ticket)
  2. 9 pre-existing opt_out test failures need cleanup ticket
## Validation: FAIL Tiers executed: Tier 1 (local tests), Tier 3 (prod pod/health check) Validation note: `validation-293-2026-04-03` 13 checks: 10 PASS, 3 FAIL **Failures:** - **Dockerfile build (blocker):** Line 11 runs `npx mjml 'templates/email/*.mjml' -o templates/email/compiled/` but the `compiled/` directory does not exist in the Docker build stage. MJML CLI errors: "Multiple input files, but output option should be either an existing directory or an empty string." Fix: add `RUN mkdir -p templates/email/compiled` before the compile step. - **CI pipeline #288:** `build-and-push` step failed (mkdir bug above). `test` step also failed with 9 pre-existing opt_out test failures (from #263, not this PR). `update-kustomize-tag` skipped. - **Image not deployed:** Pod still running old image `23902fe5...`. Merge commit `cf37b1b` was never built/pushed. **No production regression** — existing pod is healthy (0 restarts, /healthz 200). New code never deployed. **Discovered scope:** 1. Dockerfile mkdir fix (blocker for this ticket) 2. 9 pre-existing opt_out test failures need cleanup ticket
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#293
No description provided.