feat(emails): shared house-style email layout wrapper (consistency pass) - #784
Conversation
Welcome, valuation, and weekly-report emails should share one visual language (DESIGN.md — four-font system, shadow-as-border, achromatic chrome), but each outbound email was assembled ad hoc (body + bare footer). This establishes the shared template (recoupable/chat#1885 consistency pass). - `renderEmailLayout` (`lib/emails/`) — one header + footer + optional-CTA wrapper: Recoup wordmark header, achromatic shadow-as-border card, the DESIGN.md font stack, centered fixed-max-width container. Email-client-safe (inline styles, literal hex tokens, webfonts degrade to system fonts). - `processAndSendEmail` — adopts the wrapper as the proof point. This is the path the live weekly-report email flows through (agent `send_email` → `processAndSendEmail`), so the already-live weekly report now renders in the shared house style, with the existing footer carried in as the layout footer. Welcome (api#774) and valuation (api#773) emails adopt the same wrapper once merged — this PR establishes the shared template. TDD: RED→GREEN for the layout renderer (body/footer/CTA + house-style markers) and for the processAndSendEmail adoption. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Bugbot is not enabled for your account, so this pull request was not reviewed. Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs. |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning Review limit reached
Next review available in: 53 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (4)
📒 Files selected for processing (4)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
2 issues found across 4 files
Confidence score: 3/5
- In
lib/emails/renderEmailLayout.ts,renderEmailLayoutinterpolates CTA label/URL without HTML escaping, so untrusted values could inject markup into email content and create a phishing or content-integrity risk—escape or sanitize both fields before template insertion. - In
lib/emails/renderEmailLayout.ts, the footer wrapper adds top spacing on top of the<hr>margin already included bygetEmailFooter(), which can cause inconsistent footer rendering and excess whitespace across email clients—remove one of the top-spacing sources to keep layout spacing predictable.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="lib/emails/renderEmailLayout.ts">
<violation number="1" location="lib/emails/renderEmailLayout.ts:55">
P2: CTA label and URL are not HTML-escaped before interpolation into the email template. If these values ever come from user input, scraper data, or a database field, an attacker could inject arbitrary markup or break the href attribute. The codebase already has escapeHtml in lib/emails/escapeHtml.ts — use it here to guard cta.label and cta.url.</violation>
<violation number="2" location="lib/emails/renderEmailLayout.ts:62">
P2: The layout's footer wrapper adds its own `padding-top:24px;margin-top:24px` around the footer HTML, but `getEmailFooter()` already ships an `<hr>` with `margin-top:24px`. This doubles the visual gap above the footer in the final email. Consider removing `padding-top` and `margin-top` from the footer wrapper div in `renderEmailLayout` and letting the footer's own `<hr>` margin control the spacing, or removing the `<hr>`'s top margin if the wrapper is the sole spacer.</violation>
</file>
Architecture diagram
sequenceDiagram
participant Agent as Agent (send_email)
participant API as API Route (processAndSendEmail)
participant Layout as renderEmailLayout
participant Footer as getEmailFooter
participant DB as Database
participant Resend as Resend API
participant Client as Email Client
Note over Agent,Client: Weekly Report Email Flow (Proof Point)
Agent->>API: POST /process (subject, html, room_id)
API->>DB: selectRoomWithArtist(room_id)
DB-->>API: Room data
API->>Footer: getEmailFooter(room_id, artist_name)
Footer-->>API: Footer HTML
API->>Layout: renderEmailLayout({ bodyHtml, footerHtml })
Note over Layout: Wraps body + footer in house style<br/>Recoup wordmark header<br/>Shadow-as-border card<br/>Plus Jakarta Sans font stack<br/>Achromatic brand tokens<br/>Centered max-width container
Layout-->>API: Full HTML with layout
API->>Resend: sendEmailWithResend({ html: htmlWithLayout })
Resend-->>API: Email sent (id)
API-->>Agent: Success response
Note over Client: Email Rendered
Client->>Client: Render inline styles<br/>Wordmark header<br/>Body content (preserved)<br/>Optional footer<br/>All styles literal hex (no CSS vars)
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| const ctaBlock = cta | ||
| ? ` | ||
| <div style="padding:24px 0 4px;"> | ||
| <a href="${cta.url}" target="_blank" rel="noopener noreferrer" |
There was a problem hiding this comment.
P2: CTA label and URL are not HTML-escaped before interpolation into the email template. If these values ever come from user input, scraper data, or a database field, an attacker could inject arbitrary markup or break the href attribute. The codebase already has escapeHtml in lib/emails/escapeHtml.ts — use it here to guard cta.label and cta.url.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/emails/renderEmailLayout.ts, line 55:
<comment>CTA label and URL are not HTML-escaped before interpolation into the email template. If these values ever come from user input, scraper data, or a database field, an attacker could inject arbitrary markup or break the href attribute. The codebase already has escapeHtml in lib/emails/escapeHtml.ts — use it here to guard cta.label and cta.url.</comment>
<file context>
@@ -0,0 +1,83 @@
+ const ctaBlock = cta
+ ? `
+<div style="padding:24px 0 4px;">
+ <a href="${cta.url}" target="_blank" rel="noopener noreferrer"
+ style="display:inline-block;background:${CTA_BG};color:${CTA_FG};font-family:${FONT_STACK};font-weight:600;font-size:14px;text-decoration:none;padding:12px 20px;border-radius:8px;">
+ ${cta.label}
</file context>
| </div>`.trim() | ||
| : ""; | ||
|
|
||
| const footerBlock = footerHtml |
There was a problem hiding this comment.
P2: The layout's footer wrapper adds its own padding-top:24px;margin-top:24px around the footer HTML, but getEmailFooter() already ships an <hr> with margin-top:24px. This doubles the visual gap above the footer in the final email. Consider removing padding-top and margin-top from the footer wrapper div in renderEmailLayout and letting the footer's own <hr> margin control the spacing, or removing the <hr>'s top margin if the wrapper is the sole spacer.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/emails/renderEmailLayout.ts, line 62:
<comment>The layout's footer wrapper adds its own `padding-top:24px;margin-top:24px` around the footer HTML, but `getEmailFooter()` already ships an `<hr>` with `margin-top:24px`. This doubles the visual gap above the footer in the final email. Consider removing `padding-top` and `margin-top` from the footer wrapper div in `renderEmailLayout` and letting the footer's own `<hr>` margin control the spacing, or removing the `<hr>`'s top margin if the wrapper is the sole spacer.</comment>
<file context>
@@ -0,0 +1,83 @@
+</div>`.trim()
+ : "";
+
+ const footerBlock = footerHtml
+ ? `
+<div style="padding-top:24px;margin-top:24px;">
</file context>
…t-stack quoting Completes the consistency pass — previously the wrapper was only adopted in the weekly-report path, so welcome/valuation still shipped their own chrome. - buildWelcomeEmail + renderValuationReportHtml now render through renderEmailLayout (body + cta + footer), dropping their duplicated outer page/card chrome; each keeps only its own content. - Fix: FONT_STACK used double-quoted font names inside double-quoted style="…" attributes, which terminated the attribute early — silently dropping the font (clients fell back to serif) AND every declaration after it (the CTA button lost its background/padding/radius). Single-quote the font names; add a regression test. - Layout footer tagline: em dash -> comma (house copy rule). - Consistency assertions added to both email tests. 199 lib/emails tests pass; lint + format clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
0 issues found across 6 files (changes from recent commits).
Confidence score: 5/5
- Automated review surfaced no issues in the provided summaries.
- No files require special attention.
Requires human review: Auto-approval blocked by 2 unresolved issues from previous reviews.
Re-trigger cubic
Completed the consistency pass + fixed a layout bug (
|
Preview verification — 2026-07-27Preview: https://api-aixyt7llf-recoup.vercel.app Results
The layout, before and after
This branch Recoup wordmark header, white card on the Note on row 2 — validation precedes auth (pre-existing, not this PR)An unauthenticated Note on row 6 — what I could not verify, and whyI could not independently inspect the delivered HTML. {"subject":"[api#784 preview test] shared email layout wrapper","text":"## Layout wrapper check…"}So substring checks against it are worthless here — worse, they produce false positives ( What is established: the send went through the preview built from this PR's head, returned 200, and logged |
Gap closed: the other two surfaces (welcome + valuation)My previous comment only evidenced one of the three surfaces this PR touches. The chat#1889 matrix row reads "Shared Each is produced by calling this branch's actual exported renderer, so what you see is byte-for-byte what the deployed code emits. Welcome —
|
| Surface | Entry point | How verified | Result |
|---|---|---|---|
| Weekly report / agent emails | processAndSendEmail |
Live send through the preview (e3fbd069) → Resend baa34a04, email_send_log 88569abb status=sent |
✅ |
| Welcome | buildWelcomeEmail |
Rendered from the branch, screenshot above | ✅ |
| Valuation | renderValuationReportHtml |
Rendered from the branch, screenshot above | ✅ |
One shared wrapper across all three: #f7f7f7 canvas, white card with shadow-as-border (no CSS border), Plus Jakarta Sans + system fallbacks, wordmark header, per-email footer below a divider, platform line outside the card. The font-stack quoting fix in this PR is load-bearing for every one of them — single-quoted family names, because the stack is interpolated into double-quoted style="…" attributes and double quotes would terminate the attribute early, silently dropping the font and every declaration after it.
Unrelated observation, for api#787
The welcome email above still renders five numbered steps, including "4. See your baseline valuation." That's the count api#787 (PR D) reduces to four so app and email agree, presenting the valuation as the payoff instead. Nothing to change here — noting it since this screenshot is the current-state baseline that PR will be diffed against.






What
Welcome, valuation, and weekly-report emails should share one visual language (DESIGN.md — four-font system, shadow-as-border, achromatic chrome), but each outbound email was assembled ad hoc (rendered body + a bare footer). This is the consistency pass from recoupable/chat#1885 — it establishes the shared email template.
Changes
renderEmailLayout(lib/emails/) — one sharedheader + footer + optional-CTAwrapper:box-shadow, not a CSSborder, per DESIGN.md)processAndSendEmail— adopts the wrapper as the proof point. This is the path the live weekly-report email flows through (agentsend_email→processAndSendEmail), so the already-live weekly report now renders in the shared house style, with the existinggetEmailFooteroutput carried in as the layout footer.Note
The welcome (api#774) and valuation (api#773) emails will adopt this same
renderEmailLayoutwrapper once merged — this PR establishes the shared template they align to, so all three read as one family.Verification
renderEmailLayout— wraps body; includes footer when given / omits when not; renders CTA button (label + href) when given / none otherwise; carries house-style markers (wordmark,#0a0a0aink,box-shadowcard, Plus Jakarta Sans,max-width).processAndSendEmail— the sent HTML preserves the body and is wrapped in the shared layout (wordmark + shadow-as-border + font stack).lib/emailsdomain suite green — 159 tests passing (existingprocessAndSendEmailbehavior unchanged).tsc --noEmit: no errors in changed files (1 pre-existing baseline error inprocessAndSendEmail.test.tsis unchanged, only line-shifted).eslint+prettierclean on all changed files.Targets main.
Relates to recoupable/chat#1885 (consistency pass across onboarding emails).
🤖 Generated with Claude Code
Summary by cubic
Add a shared house‑style email layout and adopt it across weekly report, welcome, and valuation emails. This completes the consistency pass so onboarding emails use one visual language (recoupable/chat#1885).
New Features
lib/emails/renderEmailLayout: shared wrapper (header, footer, optional CTA) with Recoup wordmark, shadow‑as‑border card (box-shadow), DESIGN.md font stack, centered max‑width container, inline styles, and literal hex tokens.lib/emails/processAndSendEmail,lib/emails/buildWelcomeEmail, andlib/emails/valuationReport/renderValuationReportHtml; these now provide only body/CTA/footer and render through the new layout.Bug Fixes
style="…"in email clients; added a regression test inrenderEmailLayout.test.ts.Written for commit e3fbd06. Summary will update on new commits.