feat: MJML email template system with jersey reminder #2
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "1-feat-mjml-email-template-system-with-jer"
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?
Summary
MJML email template system for Westside Kings & Queens. First template is the girls jersey ordering reminder -- branded, professional, with real jersey images from MinIO CDN. Templates compile to HTML with literal
{{variable}}placeholders that basketball-api replaces at send time via simple string substitution.Changes
package.json-- mjml dependency, build and preview scriptsscripts/build.js-- Node build script compiling src/.mjml to dist/.htmlsrc/base-layout.mjml-- shared Westside brand wrapper (logo, red accent header, dark theme, footer)src/jersey-reminder.mjml-- jersey ordering reminder with three options ($90 reversible, $130 jersey+warmup, $0 opt-out), real jersey photos from MinIO CDN, gentle tonesample-data/jersey-reminder.json-- sample data for preview renderingpreview/index.html-- browser preview that loads compiled HTML with sample data.gitignore-- excludes node_modules and distpackage-lock.json-- lockfile for reproducible buildsTest Plan
npm install && npm run buildexits 0, produces dist/jersey-reminder.html and dist/base-layout.htmldist/jersey-reminder.htmlcontains{{name}}and{{jersey_url}}placeholdersnpm run previewopens browser preview at localhost:3001Review Checklist
Related
plan-wkq-- Phase 11 discovered scope- base-layout.mjml: shared Westside brand wrapper with logo, red accent, footer - jersey-reminder.mjml: girls jersey ordering reminder with three options ($90/$130/opt-out), jersey images from MinIO CDN, gentle tone, no pressure - Build script compiles src/*.mjml to dist/*.html via node - Preview system loads compiled HTML with sample data in browser - Brand colors: black #1a1a1a, red #c41230, white #ffffff - {{name}} and {{jersey_url}} placeholders for simple string replacement at send time Closes #1 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>Review-fix loop
No issues found. All files are new (greenfield repo).
Verified:
npm run buildexits 0, compiles bothbase-layout.mjmlandjersey-reminder.mjmltodist/dist/jersey-reminder.htmlcontains{{name}}(1 occurrence) and{{jersey_url}}(2 occurrences) placeholdersIMG_4164.jpeg,IMG_4165.jpeg) return HTTP 200#1a1a1a(black),#c41230(red),#ffffff(white)PR #2 Review
DOMAIN REVIEW
Tech stack: Node.js, MJML (email templating), vanilla HTML/JS preview system.
This is a new repo (
westside-emails) introducing an MJML-based email template compilation pipeline. The architecture is: MJML source files compile to HTML vianpm run build, producingdist/*.htmlfiles with{{variable}}placeholders that basketball-api will consume for string substitution before sending via Gmail OAuth.Build script (
scripts/build.js):mjmlwithvalidationLevel: "soft", which is appropriate for development but means warnings do not fail the build. Files with errors are still written to disk -- the script increments anerrorscounter but never exits non-zero for compilation warnings. This is acceptable for a template repo where the human previews output, but worth noting.fs.readFileSync/fs.writeFileSync-- fine for a build tool, not a server.Base layout (
src/base-layout.mjml):{{content}}placeholder in the body section.Jersey reminder (
src/jersey-reminder.mjml):<mj-include path="./base-layout.mjml" />or any include mechanism. Instead, it duplicates the entire header (logo, org name, red accent bar), all<mj-head>attributes/styles, and the footer verbatim frombase-layout.mjml. The base layout file exists but is never referenced by the actual template.base-layout.mjmlcompiles to its owndist/base-layout.html(with a raw{{content}}placeholder), butjersey-reminder.mjmlis a fully standalone file that copy-pastes the layout. When branding changes (logo URL, colors, footer text), both files must be updated independently -- exactly the divergence risk that base layouts are designed to prevent.{{name}}and{{jersey_url}}placeholders correctly.Preview system (
preview/index.html):renderTemplate()uses regex replacement for{{key}}patterns -- correct and sufficient for simple string substitution.npm run build.srcdocapproach avoids CORS issues with local file serving.Sample data (
sample-data/jersey-reminder.json):https://westsidekingsandqueens.tail5b443a.ts.net/jersey?token=sample-token-abc123. This is sample/preview data, not a real credential, and the file is clearly labeled as sample data. Acceptable.Package configuration:
^4.15.3(caret range allows minor/patch updates).package-lock.jsonis committed for reproducibility. Good.serveas devDependency for preview. Clean dependency footprint.BLOCKERS
1. DRY violation:
base-layout.mjmlexists but is not used (NOT a security/auth path, but a significant architecture concern)src/base-layout.mjmldefines the shared branding layout with a{{content}}slot. However,src/jersey-reminder.mjmlduplicates the entire header, mj-head attributes, styles, and footer instead of using<mj-include>. This means:base-layout.mjmlcompiles to an unusable HTML file (raw{{content}}in output)MJML supports
<mj-include path="./base-layout.mjml" />natively. The jersey-reminder template should compose with the base layout rather than duplicating it. This is the stated architectural goal in issue #134 ("brand wrapper") and the repo's own file naming convention implies composition.2. No test coverage for build output
The Test Plan states "Build succeeds and outputs both compiled HTML files" and "Placeholders present:
{{name}}and{{jersey_url}}" -- but these are manual verification steps, not automated tests. There are no test scripts, no CI pipeline (.woodpecker.ymlis absent), and no assertions that:For a template system that basketball-api will consume programmatically, build correctness should be verified automatically. At minimum, a smoke test that runs
npm run buildand asserts output files exist with expected placeholders would prevent silent regressions. Per BLOCKER criteria: new functionality with zero test coverage.NITS
README is minimal. A template repo benefits from documenting: how to add a new template, the placeholder convention, how basketball-api consumes the output, and the preview workflow. The current README is one line.
Build script exit code.
scripts/build.jsexits 0 even when templates have compilation warnings. Considerprocess.exit(1)whenerrors > 0so CI (when added) can catch regressions.No
.woodpecker.yml. This repo lives on Forgejo but has no CI pipeline. Even a simple pipeline that runsnpm ci && npm run buildwould catch broken templates on push.base-layout.mjmlcompiles to a broken HTML file. Since it contains{{content}}as literal text (not a valid MJML construct), it produces an HTML file with a raw{{content}}string in the output. If it is meant only as an include fragment, it should either be excluded from the build glob (e.g., prefix with_and filter in build.js) or moved to apartials/directory.Hardcoded MinIO CDN URLs in templates. The logo and jersey image URLs point to
minio-api.tail5b443a.ts.net. If the MinIO hostname changes, every template needs updating. Consider a{{cdn_base_url}}placeholder or a build-time variable, though this may be acceptable given the current scale.Preview iframe height is hardcoded to 1200px. The jersey-reminder email is long with three product cards. This works now but may clip or leave excess whitespace for shorter/longer future templates. Not blocking.
SOP COMPLIANCE
1-feat-mjml-email-template-system-with-jerreferences issue #1PROCESS OBSERVATIONS
npm run build. For a system that basketball-api depends on at runtime, this is a reliability concern.VERDICT: NOT APPROVED
Two blockers must be addressed:
jersey-reminder.mjmlmust compose withbase-layout.mjmlvia<mj-include>rather than duplicating the entire layout. The base layout file exists for this purpose -- use it.npm run buildproduces expected output files with expected placeholders. Adding a.woodpecker.ymlpipeline would be ideal but atestscript inpackage.jsonis the minimum bar.Refactor jersey-reminder.mjml to use mj-include for the shared head attributes, header, and footer instead of duplicating them from base-layout.mjml. Extract three partials (head.mjml, header.mjml, footer.mjml) into src/partials/ that both templates now include. Add scripts/test.js with 8 assertions: strict MJML compilation, dist output verification, placeholder marker checks ({{name}}, {{jersey_url}}), and DRY verification that partials exist and mj-include directives are used. Wire up npm test in package.json. Add .woodpecker.yaml CI pipeline (install, build, test steps). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>QA Blocker Fixes
Blocker 1 -- DRY violation (resolved)
Extracted three shared partials into
src/partials/:head.mjml-- mj-attributes (font family, colors, line-height) and mj-style (cta-link, footer-link)header.mjml-- logo image, "Westside Kings & Queens" title, red accent dividerfooter.mjml-- org name and locationBoth
base-layout.mjmlandjersey-reminder.mjmlnow use<mj-include path="./partials/..." />instead of duplicating these blocks. The jersey reminder template dropped from 217 lines to 166, with zero inline duplication of brand elements.Blocker 2 -- Zero test coverage (resolved)
scripts/test.js-- 8 assertions covering strict MJML compilation, dist output existence, placeholder markers ({{name}},{{jersey_url}}), and DRY verification (partials exist, mj-include directives used)package.json-- added"test": "node scripts/build.js && node scripts/test.js"script.woodpecker.yaml-- CI pipeline with install, build, and test steps usingnode:20-alpineAll 8 tests pass. Build output verified: compiled HTML preserves all content, placeholders, header, and footer.
PR #2 Review
Re-review after blocker fixes. Previous review found two blockers: (1) DRY violation in jersey-reminder.mjml, (2) no tests or CI. Both addressed in commit
60d2792.Tech stack: Node.js, MJML email templating, Woodpecker CI. Applying generic code quality + Node/CI domain checks.
DOMAIN REVIEW
MJML Architecture (13 files, +3447 lines)
The template system is well-structured. Two templates (
jersey-reminder.mjml,base-layout.mjml) share three partials viamj-include:src/partials/head.mjml-- shared mj-attributes (font family, colors, background) and mj-stylesrc/partials/header.mjml-- logo image from MinIO CDN, org name, red accent dividersrc/partials/footer.mjml-- org name + locationThe build pipeline (
scripts/build.js) compiles allsrc/*.mjmltodist/*.htmlwith soft validation. The test suite (scripts/test.js) has 8 assertions across 4 categories:{{name}},{{jersey_url}}survive compilation)The DRY test in category 4 is a smart guardrail -- it will catch future regressions if someone inlines partials again.
Build script review (
scripts/build.js):validationLevel: "soft"which allows compilation despite warnings. This is appropriate for build output (produce HTML even with non-critical warnings) while tests use"strict"to catch issues. Good separation.Test script review (
scripts/test.js):npm run buildhaving been run first -- this is handled by thepackage.jsontest script:"test": "node scripts/build.js && node scripts/test.js".jersey-reminder.mjml. When future templates are added, this test should be generalized. Acceptable for now with one template.CI pipeline (
.woodpecker.yaml):node:20-alpine.npm ciin step 1 populatesnode_modules/, and steps 2-3 will also have access since Woodpecker shares the workspace volume by default. Correct..woodpecker.yamlis ready but CI activation is a separate step.Preview system (
preview/index.html):const templates = { ... }) makes it easy to add new templates.{{key}}replacement is simple and sufficient for the use case. No risk of injection since sample data is developer-controlled JSON files.Email content quality:
minio-api.tail5b443a.ts.net/assets/westside/jerseys/). These are real, hosted assets.minio-api.tail5b443a.ts.net/assets/westside/branding/logo.jpeg. Consistent CDN usage.alttext present on all images. Good accessibility.BLOCKERS
None. Both previously identified blockers have been resolved:
DRY violation -- FIXED.
jersey-reminder.mjmlnow uses 3mj-includedirectives forhead.mjml,header.mjml, andfooter.mjml. Thebase-layout.mjmlalso uses the same 3 includes. Branding changes propagate from one place.No tests or CI -- FIXED. 8-assertion test suite added (
scripts/test.js), wired via"test": "node scripts/build.js && node scripts/test.js"inpackage.json. Woodpecker CI pipeline configured in.woodpecker.yaml. Test categories cover compilation, output, placeholders, and DRY enforcement.NITS
CI not yet activated. The
.woodpecker.yamlis present but the repo is not registered with Woodpecker (returns 404). This should be activated before merge so the pipeline validates the branch. If activation is planned as a follow-up, track it.base-layout.mjmlis unused. It defines a{{content}}slot pattern but no template inherits from it --jersey-reminder.mjmluses the partials directly instead of extendingbase-layout.mjml. This file should either be used as the actual base (future templates extend it) or removed to avoid confusion. If it's a reference/scaffold for future templates, a comment at the top saying so would help.Hardcoded MinIO hostname. Image URLs use
minio-api.tail5b443a.ts.netdirectly in MJML source. If the CDN hostname ever changes, every template needs updating. Consider documenting this as a known constraint, or using a placeholder like{{cdn_base}}that the build or send-time process replaces.head.mjmlpartial hasmj-sectionbackground#1a1a1abutjersey-reminder.mjmlsections use#141414. The partial sets<mj-section background-color="#1a1a1a" />as default, but every section in the jersey template overrides it to#141414. The default in the partial could be updated to#141414to match actual usage and reduce override noise.Test script hardcodes
jersey-reminder.mjmlfilename. Test 3 (placeholder check) and Test 4 (DRY check) reference this specific file. When a second template is added, these tests should be generalized to loop over all templates with a corresponding sample-data JSON.SOP COMPLIANCE
1-feat-mjml-email-template-system-with-jer-- references issue #1 in westside-emails)plan-wkq-- Phase 11 discovered scope)forgejo_admin/basketball-api#134)feat:for initial,fix:for blocker resolution).gitignoreexcludesdist/andnode_modules/PROCESS OBSERVATIONS
westside-emails) that decouples email templates frombasketball-apiPython code. Good architectural separation -- email copy changes no longer require API redeployment. Positive DORA impact on DF.basketball-apiside of this (template loader, replacing f-string HTML) is tracked in issue #134 and PR #135. This PR is the template repo half. The two PRs should be merged in sequence: templates first, then the API integration.VERDICT: APPROVED