Skip to content

Commit 856e726

Browse files
Add email-playground as the visual source-of-truth for email templates (#2)
Introduces a React Email + Tailwind TypeScript project under email-playground/ that owns the newsletter design. A generic build script renders tokenized HTML into src/templates/ (gitignored), which Python fills at send time instead of hand-building HTML. The template is regenerated in CI and the multi-stage Dockerfile so the runtime image always gets a fresh build.
1 parent bf72577 commit 856e726

19 files changed

Lines changed: 4822 additions & 81 deletions

.dockerignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,13 @@ env/
2121
.ruff_cache/
2222
.pytest_cache/
2323

24+
# Node (email-playground): keep the source for the build stage, drop the heavy/generated bits.
25+
node_modules/
26+
.react-email/
27+
email-playground/out/
28+
# Built fresh by the Docker template-builder stage, so never ship a local copy.
29+
/src/templates/*.html
30+
2431
# OS / editor
2532
.DS_Store
2633
.idea/

.env.example

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ SUBJECT_TTL=604800
1212

1313
# Email delivery (Resend). Only needed when sending: python main.py GOTO you@example.com
1414
RESEND_API_KEY=
15-
EMAIL_FROM=Mediapulse <onboarding@resend.dev>
15+
EMAIL_FROM=MediaPulse <onboarding@resend.dev>
1616

1717
# Subscriber + ticker database (Postgres/Supabase, read-only). Used by the campaign job and to
1818
# enrich the analyst with each ticker's listing profile (sector, business, HQ). Skipped if unset.

.github/workflows/code-quality.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,16 @@ jobs:
3636
with:
3737
python-version: "3.11"
3838

39+
- uses: actions/setup-node@v4
40+
with:
41+
node-version: "20"
42+
43+
- name: Build email templates
44+
working-directory: email-playground
45+
run: |
46+
npm ci
47+
npm run build:templates
48+
3949
- name: Install dependencies
4050
run: pip install -r requirements.txt -r requirements-dev.txt
4151

.gitignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,10 @@ env/
2525
.vscode/
2626

2727
.subject_cache.json
28+
29+
# Generated email templates (built from email-playground, fresh each deploy/CI run)
30+
/src/templates/*.html
31+
32+
# Node (email-playground)
33+
node_modules/
34+
.react-email/

CLAUDE.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ All application code lives under `src/`. Packages keep their top-level names (`a
1515
- `src/agents/orchestrator.py` — the pipeline: analyst → 5 parallel beat desks (researcher → writer → editor) → managing-editor gap roundtable → masthead → reviewer → deterministic clean/assemble/dedupe. Most non-agent logic (citation gating, URL/article validation, dedupe, subject-name canonicalization, prose humanizing) lives here.
1616
- `src/agents/` — one module per agent (`analyst`, `researcher`, `writer`, `editor`, `managing_editor`, `reviewer`), plus `beats.py` (beat desks), `campaign.py` (top-level run over subscriptions), and `tools/` (Serper search, web fetch).
1717
- `src/agents/skills/``SKILL.md` files that control agent behavior (`subject-profile`, `section-research`, `newsletter-format`). Prefer editing these over code when changing how agents research or write.
18-
- `src/utils/``db.py` (subscriptions/tickers from the Mediapulse Postgres), `memory.py`, `client.py`, `guardrails.py`, `sections.py`, `mailer.py`, `email_template.py`, `ticker.py`.
18+
- `src/utils/``db.py` (subscriptions/tickers from the MediaPulse Postgres), `memory.py`, `client.py`, `guardrails.py`, `sections.py`, `mailer.py`, `email_template.py`, `ticker.py`. `email_template.py` no longer hand-builds HTML: it parses the newsletter markdown and fills the tokenized template at `src/templates/newsletter.html`.
19+
- `email-playground/` — a standalone React Email (TypeScript) project that is the visual source-of-truth for MediaPulse email templates (the newsletter is the first one). `npm run build:templates` renders each template to a tokenized `src/templates/<name>.html` that the Python side consumes (`email_template.py` for the newsletter). Those HTML files are gitignored build artifacts, regenerated fresh in CI and the Docker build. Restyle emails there, not in Python. See `email-playground/README.md`.
1920
- `tests/` — pytest suite covering the deterministic logic (see Testing below).
2021

2122
## Setup
@@ -56,6 +57,12 @@ pytest # whole suite
5657
conda run -n agentic-mediapulse pytest # if pytest is not on PATH
5758
```
5859

60+
The email-template tests read `src/templates/newsletter.html`, which is gitignored and generated by the playground. Build it once before running the suite locally (CI does this in the `tests` job):
61+
62+
```
63+
cd email-playground && npm install && npm run build:templates
64+
```
65+
5966
Config is in `pyproject.toml` under `[tool.pytest.ini_options]`: `pythonpath = ["src"]`, `testpaths = ["tests"]`, `asyncio_mode = "auto"` (async tests need no decorator). CI runs `pytest` in a separate `tests` job after installing both requirement files.
6067

6168
The suite covers the deterministic logic, not the LLM agents: orchestrator text/section helpers, the email template, Serper tools, the DB profile shaping, guardrail middleware, memory/ticker context providers, the mailer, the client model resolution, and the campaign delivery flow. Conventions for new tests:
@@ -78,5 +85,5 @@ The suite covers the deterministic logic, not the LLM agents: orchestrator text/
7885
## External services
7986

8087
- `SERPER_API_KEY` — web search.
81-
- `MEDIAPULSE_DATABASE_URL` — Postgres for subscriptions and ticker data; schema is defined in the upstream [Mediapulse](https://github.com/hyperjumptech/mediapulse) repo.
88+
- `MEDIAPULSE_DATABASE_URL` — Postgres for subscriptions and ticker data; schema is defined in the upstream [MediaPulse](https://github.com/hyperjumptech/mediapulse) repo.
8289
- `SECRET_KEY` — API auth.

Dockerfile

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
# syntax=docker/dockerfile:1
2+
3+
FROM node:20-slim AS template-builder
4+
WORKDIR /build/email-playground
5+
COPY email-playground/package.json email-playground/package-lock.json ./
6+
RUN npm ci
7+
COPY email-playground/ ./
8+
RUN npm run build:templates
9+
110
FROM python:3.11-slim
211

312
ENV PYTHONDONTWRITEBYTECODE=1 \
@@ -9,6 +18,7 @@ COPY requirements.txt .
918
RUN pip install --no-cache-dir -r requirements.txt
1019

1120
COPY . .
21+
COPY --from=template-builder /build/src/templates/ src/templates/
1222

1323
RUN useradd --no-create-home --shell /bin/false app \
1424
&& chown -R app:app /app

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# agentic-mediapulse
22

3-
A simpler, agentic version of [Mediapulse](https://github.com/hyperjumptech/mediapulse). Give it a subject — a stock ticker, a company name, or an industry theme — and a newsroom of focused agents researches, writes, and edits a locale-aware briefing across five editorial sections, with every claim traced to a real source.
3+
A simpler, agentic version of [MediaPulse](https://github.com/hyperjumptech/mediapulse). Give it a subject — a stock ticker, a company name, or an industry theme — and a newsroom of focused agents researches, writes, and edits a locale-aware briefing across five editorial sections, with every claim traced to a real source.
44

55
## Setup
66

@@ -34,7 +34,7 @@ A simpler, agentic version of [Mediapulse](https://github.com/hyperjumptech/medi
3434
| `REDIS_URL` | Subject-memory store — default `redis://localhost:6379/0` | Yes |
3535
| `MEDIAPULSE_DATABASE_URL` | Postgres connection string (read-only) | Yes |
3636
| `RESEND_API_KEY` | Resend key for email delivery | Yes |
37-
| `EMAIL_FROM` | Sender address, e.g. `Mediapulse <hello@example.com>` | Yes |
37+
| `EMAIL_FROM` | Sender address, e.g. `MediaPulse <hello@example.com>` | Yes |
3838
| `SECRET_KEY` | Required on every API request (`X-API-Key` header) | Yes |
3939
| `ANALYST_MODEL` | Model override for the analyst — falls back to `OPENAI_MODEL` | No |
4040
| `RESEARCHER_MODEL` | Model override for the researcher — falls back to `OPENAI_MODEL` | No |
@@ -120,4 +120,4 @@ To change how an agent writes or researches, edit its skill file in `src/agents/
120120

121121
## Database
122122

123-
Subscriptions and ticker data are read from the [Mediapulse](https://github.com/hyperjumptech/mediapulse) database. Set `MEDIAPULSE_DATABASE_URL` to your Mediapulse Postgres connection string — the schema is defined in that repo.
123+
Subscriptions and ticker data are read from the [MediaPulse](https://github.com/hyperjumptech/mediapulse) database. Set `MEDIAPULSE_DATABASE_URL` to your MediaPulse Postgres connection string — the schema is defined in that repo.

email-playground/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
node_modules/
2+
.react-email/
3+
out/

email-playground/README.md

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# email-playground
2+
3+
The home for MediaPulse's [React Email](https://react.email/components) templates and their live
4+
preview. Each template lives as a React component under `emails/`. A template can also export a
5+
tokenized HTML build that the Python app fills at send time. The newsletter is the first such
6+
template; others can be added the same way.
7+
8+
## Setup
9+
10+
```
11+
npm install
12+
```
13+
14+
## Design and preview
15+
16+
```
17+
npm run dev
18+
```
19+
20+
Opens the React Email preview server at http://localhost:3000. Every component in `emails/` shows
21+
up, rendered from its `PreviewProps`. Edits hot-reload. Styling is Tailwind via react-email's
22+
`<Tailwind>` component, inlined into the HTML at render time.
23+
24+
## Build the tokenized templates
25+
26+
```
27+
npm run build:templates
28+
```
29+
30+
For every `emails/*.tsx` that exports both a default component and a `templateProps` object, this
31+
renders the component in template mode and writes `../src/templates/<name>.html`: a tokenized HTML
32+
file with `{{placeholders}}` and `<!--#region-->…<!--/region-->` blocks that the Python side fills
33+
per message. The output is a gitignored build artifact, regenerated in CI and the Docker build.
34+
**The Python tests and the running app require these files**, so run it once after `npm install`.
35+
36+
## Adding a template that the Python app can fill
37+
38+
1. Create `emails/<name>.tsx` with a default-exported component and a `PreviewProps` for the dev
39+
server.
40+
2. Add a `templateMode` prop. When set, wrap variable, repeatable, and optional regions in
41+
`<Region name="...">` (which emits `[[#name]]`/`[[/name]]` markers) and leave content as
42+
`{{token}}` placeholders.
43+
3. Export `templateProps`: the props that drive template-mode rendering.
44+
4. Run `npm run build:templates`, then fill `src/templates/<name>.html` from Python.
45+
46+
Templates without `templateProps` are preview-only and are skipped by the build.
47+
48+
## Newsletter token reference
49+
50+
| Token / region | Filled by Python with |
51+
| --------------------------------------- | ---------------------------------------------- |
52+
| `{{title}}` | title (the markdown `#` heading) |
53+
| `<!--#standfirst-->` `{{summary}}` | the standfirst paragraph (omitted if empty) |
54+
| `<!--#sections-->` | placeholder for all rendered sections |
55+
| `<!--#section-->` `{{section_name}}` | a standard section, repeated per section |
56+
| `<!--#quickhits-->` | the highlighted Quick Hits panel variant |
57+
| `<!--#sectionsep-->` | separator, kept between sections, dropped first|
58+
| `<!--#item-->` `{{item_summary}}` | repeated once per item in a section |
59+
| `<!--#itemsep-->` | separator, kept between items, dropped on first|
60+
| `<!--#readlink-->` `{{item_url}}` / `{{item_title}}` | the "Read:" citation (omitted if no URL) |
61+
| `{{footer_note}}` | the subscription footer line |
62+
| `<!--#unsubscribe-->` `{{unsubscribe_url}}` / `{{symbol}}` | unsubscribe link (omitted if no URL) |
63+
64+
The section names come from [`src/utils/sections.py`](../src/utils/sections.py); the one matching
65+
`QUICK_HITS` renders in the highlighted panel.

0 commit comments

Comments
 (0)