MJML email system: brand base + three layout templates + docker build compile #293
Labels
No labels
domain:backend
domain:devops
domain:frontend
status:approved
status:in-progress
status:needs-fix
status:qa
type:bug
type:devops
type:feature
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
forgejo_admin/basketball-api#293
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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-apiUser 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 usesload_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-emailin pal-e-docs.Deployment Coordination
When this image deploys, do NOT set
BASKETBALL_EMAIL_TEMPLATES_DIRenv 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 tokenstemplates/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 directorypackage.json— mjml dev dependency +build:emailscripttemplates/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— changeemail_templates_dirdefault to/app/templates/email/compiled/Files NOT to touch:
src/basketball_api/services/email.py— no code changes in this ticket, only templates and buildsrc/basketball_api/brand.py— tokens stay, MJML references same valuesAcceptance Criteria
{{key}}placeholders for headline, body, cta_text, cta_url, footer_notenpm run build:emailcompiles all MJML files totemplates/email/compiled/*.html/app/templates/email/compiled/load_email_template("action", {"headline": "Test", "body": "Hello", "cta_text": "Click", "cta_url": "https://example.com"})returns valid HTMLjersey-reminder.mjmlcompiles 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
load_email_template()loads each compiled layout and replaces placeholders{{key}}after replacement with complete data dictpytest tests/test_jersey_reminder.py -vConstraints
{{key}}string replacement for simplicity<mj-attributes>for shared styles, not<mj-style>(inline is safer for Gmail).gitignorethecompiled/directory — build artifact, not sourceChecklist
docker buildsucceeds locallyRelated
project-westside-basketball— westside projectarch-email— architecture referenceScope Review: NEEDS_REFINEMENT
Review note:
review-750-2026-04-03Well-scoped feature ticket with complete template sections, verified file targets, and correct repo placement. Three fixable issues:
arch-emailin pal-e-docs — board item hasarch:emaillabel but no backing note exists.BASKETBALL_EMAIL_TEMPLATES_DIRenv var must NOT be set so the new default/app/templates/email/compiled/takes effect. ConfigMap removal tracked in #753.jersey-reminder.htmlfor reference — it's in an untracked k8s ConfigMap, not in any repo.Scope Review (re-review): NEEDS_REFINEMENT
Review note:
review-750-2026-04-03-r2(child ofreview-750-2026-04-03)Three of four items from the previous review are fixed. One remains:
arch-emailarchitecture note does not exist in pal-e-docs.search_notes("arch-email")returns empty. The issue body references it, the board item has thearch:emaillabel, but the backing note was not created. Createarch-emailnote, then this ticket is READY.Scope Review: APPROVED
Review note:
review-750-2026-04-03-r3Re-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.Validation: FAIL
Tiers executed: Tier 1 (local tests), Tier 3 (prod pod/health check)
Validation note:
validation-293-2026-04-0313 checks: 10 PASS, 3 FAIL
Failures:
npx mjml 'templates/email/*.mjml' -o templates/email/compiled/but thecompiled/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: addRUN mkdir -p templates/email/compiledbefore the compile step.build-and-pushstep failed (mkdir bug above).teststep also failed with 9 pre-existing opt_out test failures (from #263, not this PR).update-kustomize-tagskipped.23902fe5.... Merge commitcf37b1bwas never built/pushed.No production regression — existing pod is healthy (0 restarts, /healthz 200). New code never deployed.
Discovered scope: