Skip to content

feat: implement beautiful jsx-email templates for OTP and team invites#196

Merged
KMKoushik merged 7 commits intomainfrom
feat/jsx-email-templates
Aug 17, 2025
Merged

feat: implement beautiful jsx-email templates for OTP and team invites#196
KMKoushik merged 7 commits intomainfrom
feat/jsx-email-templates

Conversation

@KMKoushik
Copy link
Copy Markdown
Member

@KMKoushik KMKoushik commented Aug 16, 2025

  • Add minimal, professional email templates using jsx-email
  • Create reusable email components (layout, header, footer, button)
  • Implement OTP email with prominent code display and one-click login
  • Implement team invite email with feature highlights and clear CTA
  • Update mailer service to use new jsx-email templates
  • Add development email preview endpoint for testing
  • Ensure mobile-responsive design with accessibility features

🤖 Generated with opencode

Co-Authored-By: opencode noreply@opencode.ai

Summary by CodeRabbit

  • New Features

    • Polished, branded email templates for verification codes and team invitations with clear content and single‑click action buttons.
    • Development-only email preview endpoint to validate rendered emails.
  • Improvements

    • Outgoing emails now use centralized templates for more consistent styling and accessibility.
    • Mail sending now uses rendered HTML templates.
  • Chores

    • Added an email rendering dependency.
    • Added a test script to exercise email renderers.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Aug 16, 2025

Walkthrough

Adds the "jsx-email" dependency and a set of server-side email template components and templates, an API route to preview rendered emails in development, updates mailer functions to use template rendering, an index barrel, and a test script to exercise the renderers.

Changes

Cohort / File(s) Summary of Changes
Package
apps/web/package.json
Added dependency "jsx-email": "^2.7.1".
Email template components
apps/web/src/server/email-templates/components/*
Added EmailLayout, EmailHeader, EmailFooter, and EmailButton components built with jsx-email primitives.
Email templates
apps/web/src/server/email-templates/OtpEmail.tsx, apps/web/src/server/email-templates/TeamInviteEmail.tsx
Added OtpEmail and TeamInviteEmail React components and async renderOtpEmail / renderTeamInviteEmail helpers that produce HTML strings.
Templates barrel
apps/web/src/server/email-templates/index.ts
New index file re-exporting templates and component modules.
API email preview route
apps/web/src/app/api/dev/email-preview/route.ts
New GET route that returns rendered HTML for type=otp or type=invite in development; returns JSON errors for non-dev or invalid types and handles render errors.
Mailer integration
apps/web/src/server/mailer.ts
Replaced inline HTML with calls to renderOtpEmail and renderTeamInviteEmail for sendSignUpEmail and sendTeamInviteEmail (HTML generation is now async).
Test script
apps/web/src/server/email-templates/test.ts
Added a test runner that renders OTP and team-invite templates with sample data and logs results.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant API as /api/dev/email-preview
  participant Templates as Email Templates

  Client->>API: GET /api/dev/email-preview?type=otp|invite
  API->>Templates: renderOtpEmail/renderTeamInviteEmail(sample props)
  Templates-->>API: HTML string
  API-->>Client: 200 text/html (or 400/404/500 JSON)
Loading
sequenceDiagram
  participant Caller
  participant Mailer
  participant Templates as Email Templates
  participant SMTP as Mail Transport

  Caller->>Mailer: sendSignUpEmail(...) / sendTeamInviteEmail(...)
  Mailer->>Templates: renderOtpEmail / renderTeamInviteEmail(props)
  Templates-->>Mailer: HTML string
  Mailer->>SMTP: sendMail({ subject, text, html })
  SMTP-->>Mailer: send result
  Mailer-->>Caller: resolve/return
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

I twitch my whiskers at the screen,
templates stitched in soft HTML sheen.
OTPs and invites hop in line,
previews warm like morning thine.
I nibble code and dance — hooray! 🥕🐇

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/jsx-email-templates

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.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (16)
apps/web/src/server/email-templates/components/EmailButton.tsx (3)

2-2: Remove unused import, or switch to using jsx-email's Link component.

Link is imported but not used. Either remove it or use Link instead of a raw <a> for consistency across templates.

-import { Link } from "jsx-email";
+import { Link } from "jsx-email";

If you decide not to use Link:

-import { Link } from "jsx-email";
+// no jsx-email Link needed

7-7: variant prop is declared but not used; wire it into styles.

Implement distinct styles for "primary" vs "secondary" to match the API you exposed.

 export function EmailButton({ 
   href, 
   children, 
   variant = "primary" 
 }: EmailButtonProps) {
-  return (
-    <a
-      href={href}
-      style={{
-        backgroundColor: "#000000",
-        color: "#ffffff",
-        border: "1px solid #000000",
-        borderRadius: "4px",
-        fontSize: "16px",
-        fontWeight: "500",
-        padding: "12px 24px",
-        textDecoration: "none",
-        display: "inline-block",
-        textAlign: "center" as const,
-        minWidth: "120px",
-        boxSizing: "border-box" as const,
-        float: "left" as const,
-        clear: "both" as const,
-      }}
-    >
-      {children}
-    </a>
-  );
+  const variantStyles =
+    variant === "secondary"
+      ? {
+          backgroundColor: "#ffffff",
+          color: "#000000",
+          border: "1px solid #000000",
+        }
+      : {
+          backgroundColor: "#000000",
+          color: "#ffffff",
+          border: "1px solid #000000",
+        };
+
+  return (
+    <a
+      href={href}
+      target="_blank"
+      rel="noopener noreferrer"
+      style={{
+        ...variantStyles,
+        borderRadius: "4px",
+        fontSize: "16px",
+        fontWeight: "500",
+        padding: "12px 24px",
+        textDecoration: "none",
+        display: "inline-block",
+        textAlign: "center" as const,
+        minWidth: "120px",
+        boxSizing: "border-box" as const,
+      }}
+    >
+      {children}
+    </a>
+  );
 }

31-33: Avoid floats in email CTAs; prefer inline-block or table-based centering.

Floats are inconsistently rendered across clients and can cause layout issues. The button already uses inline-block; dropping float/clear improves robustness.

-        float: "left" as const,
-        clear: "both" as const,
apps/web/src/server/email-templates/components/EmailHeader.tsx (1)

18-27: Add explicit width/height attributes for email client compatibility.

Some clients rely on attributes in addition to CSS for image sizing. Keep styles, but add attributes for reliability.

         <Img
           src={logoUrl}
           alt="Unsend"
+          width="48"
+          height="48"
           style={{
             width: "48px",
             height: "48px",
             margin: "0 0 16px 0",
             display: "block",
           }}
         />
apps/web/src/server/email-templates/components/EmailFooter.tsx (1)

29-39: Open support link safely in a new tab for most clients.

Add target and rel for security and consistency. Some email clients ignore them, but it’s harmless where unsupported.

         <a
           href={supportUrl}
+          target="_blank"
+          rel="noopener noreferrer"
           style={{
             color: "#000000",
             textDecoration: "underline",
           }}
         >
           contact our support team
         </a>
apps/web/src/server/email-templates/components/EmailLayout.tsx (2)

18-19: Set document language for better accessibility and client hints.

Adding lang aids screen readers and improves internationalization defaults.

-    <Html>
+    <Html lang="en">

63-69: Unify background color between CSS and inline Body style.

Your CSS sets body background to #f9fafb, but the Body inline style overrides to white. Align them to avoid surprises.

-      <Body style={{ backgroundColor: "#ffffff", padding: "20px" }}>
+      <Body style={{ backgroundColor: "#f9fafb", padding: "20px" }}>
apps/web/src/app/api/dev/email-preview/route.ts (2)

28-32: Prevent caching and indexing of the preview output

Add headers to ensure HTML previews aren’t cached by browsers/proxies and aren’t indexed by bots.

   return new NextResponse(html, {
     headers: {
-      "Content-Type": "text/html",
+      "Content-Type": "text/html",
+      "Cache-Control": "no-store, max-age=0",
+      "X-Robots-Tag": "noindex, nofollow",
     },
   });

1-2: Use the shared logger instead of console.error for consistency

Use the project logger for structured logs and consistent formatting.

 import { NextRequest, NextResponse } from "next/server";
-import { renderOtpEmail, renderTeamInviteEmail } from "~/server/email-templates";
+import { renderOtpEmail, renderTeamInviteEmail } from "~/server/email-templates";
+import { logger } from "~/server/logger/log";
@@
-    console.error("Error rendering email template:", error);
+    logger.error({ error }, "Error rendering email template");

Also applies to: 34-34

apps/web/src/server/email-templates/test.ts (1)

34-36: Guard for ESM builds where require may be undefined

This makes the script compatible with ESM module resolution too and avoids unhandled promise rejections.

-if (require.main === module) {
-  testEmailTemplates();
-}
+// In ESM builds, `require` may be undefined; guard accordingly.
+if (typeof require !== "undefined" && require.main === module) {
+  void testEmailTemplates();
+}
apps/web/src/server/email-templates/OtpEmail.tsx (3)

1-7: Combine jsx-email imports to reduce duplication

Minor cleanup to import from the same module once.

-import { Container, Text } from "jsx-email";
-import { render } from "jsx-email";
+import { Container, Text, render } from "jsx-email";

9-14: Export the props type for external consumers

Exporting OtpEmailProps allows callers to type their inputs when using renderOtpEmail.

-interface OtpEmailProps {
+export interface OtpEmailProps {
   otpCode: string;
   loginUrl: string;
   hostName?: string;
   logoUrl?: string;
 }

24-27: Personalize the header with hostName

Slightly improves clarity by reflecting the brand in the heading.

-      <EmailHeader 
+      <EmailHeader 
         logoUrl={logoUrl}
-        title="Sign in to your account" 
+        title={`Sign in to your ${hostName} account`} 
       />
apps/web/src/server/email-templates/index.ts (1)

1-7: Re-export props types for better DX

Expose the props types so callers can import them from the barrel. This depends on the source modules exporting the types.

 export { OtpEmail, renderOtpEmail } from "./OtpEmail";
 export { TeamInviteEmail, renderTeamInviteEmail } from "./TeamInviteEmail";
 
 export * from "./components/EmailLayout";
 export * from "./components/EmailHeader";
 export * from "./components/EmailFooter";
 export * from "./components/EmailButton";
+export type { OtpEmailProps } from "./OtpEmail";
+export type { TeamInviteEmailProps } from "./TeamInviteEmail";
apps/web/src/server/email-templates/TeamInviteEmail.tsx (2)

2-3: Combine imports from the same module.

Consolidate imports from "jsx-email" into a single statement to follow the repo’s import grouping/alphabetization guideline and reduce duplication.

-import { Container, Text } from "jsx-email";
-import { render } from "jsx-email";
+import { Container, Text, render } from "jsx-email";

14-14: Constrain role type to known values (optional).

If your domain has a fixed set of roles, prefer a string union or shared Role type over a free-form string to avoid unexpected values being rendered to end users.

Would you like me to wire this up to an existing Role type (if present) or propose a quick union like "member" | "admin" | "owner"?

Also applies to: 22-22

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between bb2a428 and fbdcc31.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (11)
  • apps/web/package.json (2 hunks)
  • apps/web/src/app/api/dev/email-preview/route.ts (1 hunks)
  • apps/web/src/server/email-templates/OtpEmail.tsx (1 hunks)
  • apps/web/src/server/email-templates/TeamInviteEmail.tsx (1 hunks)
  • apps/web/src/server/email-templates/components/EmailButton.tsx (1 hunks)
  • apps/web/src/server/email-templates/components/EmailFooter.tsx (1 hunks)
  • apps/web/src/server/email-templates/components/EmailHeader.tsx (1 hunks)
  • apps/web/src/server/email-templates/components/EmailLayout.tsx (1 hunks)
  • apps/web/src/server/email-templates/index.ts (1 hunks)
  • apps/web/src/server/email-templates/test.ts (1 hunks)
  • apps/web/src/server/mailer.ts (3 hunks)
🧰 Additional context used
📓 Path-based instructions (6)
{apps,packages}/**/*.{js,jsx,ts,tsx,css,scss,md,mdx}

📄 CodeRabbit Inference Engine (CLAUDE.md)

Use Prettier with the Tailwind plugin for code formatting

Files:

  • apps/web/src/server/email-templates/test.ts
  • apps/web/src/app/api/dev/email-preview/route.ts
  • apps/web/src/server/email-templates/components/EmailLayout.tsx
  • apps/web/src/server/mailer.ts
  • apps/web/src/server/email-templates/components/EmailButton.tsx
  • apps/web/src/server/email-templates/components/EmailFooter.tsx
  • apps/web/src/server/email-templates/index.ts
  • apps/web/src/server/email-templates/OtpEmail.tsx
  • apps/web/src/server/email-templates/components/EmailHeader.tsx
  • apps/web/src/server/email-templates/TeamInviteEmail.tsx
{apps,packages}/**/*.{js,jsx,ts,tsx}

📄 CodeRabbit Inference Engine (CLAUDE.md)

{apps,packages}/**/*.{js,jsx,ts,tsx}: Group imports by source (internal/external) and alphabetize them
Use camelCase for variables and functions, PascalCase for components and classes
Use try/catch with specific error types for error handling

Files:

  • apps/web/src/server/email-templates/test.ts
  • apps/web/src/app/api/dev/email-preview/route.ts
  • apps/web/src/server/email-templates/components/EmailLayout.tsx
  • apps/web/src/server/mailer.ts
  • apps/web/src/server/email-templates/components/EmailButton.tsx
  • apps/web/src/server/email-templates/components/EmailFooter.tsx
  • apps/web/src/server/email-templates/index.ts
  • apps/web/src/server/email-templates/OtpEmail.tsx
  • apps/web/src/server/email-templates/components/EmailHeader.tsx
  • apps/web/src/server/email-templates/TeamInviteEmail.tsx
{apps,packages}/**/*.{ts,tsx}

📄 CodeRabbit Inference Engine (CLAUDE.md)

{apps,packages}/**/*.{ts,tsx}: Use strong typing in TypeScript, avoid any, and use Zod for validation
Follow Vercel style guides with strict TypeScript

Files:

  • apps/web/src/server/email-templates/test.ts
  • apps/web/src/app/api/dev/email-preview/route.ts
  • apps/web/src/server/email-templates/components/EmailLayout.tsx
  • apps/web/src/server/mailer.ts
  • apps/web/src/server/email-templates/components/EmailButton.tsx
  • apps/web/src/server/email-templates/components/EmailFooter.tsx
  • apps/web/src/server/email-templates/index.ts
  • apps/web/src/server/email-templates/OtpEmail.tsx
  • apps/web/src/server/email-templates/components/EmailHeader.tsx
  • apps/web/src/server/email-templates/TeamInviteEmail.tsx
apps/web/**/*.{ts,tsx}

📄 CodeRabbit Inference Engine (CLAUDE.md)

Use tRPC for internal API endpoints

Files:

  • apps/web/src/server/email-templates/test.ts
  • apps/web/src/app/api/dev/email-preview/route.ts
  • apps/web/src/server/email-templates/components/EmailLayout.tsx
  • apps/web/src/server/mailer.ts
  • apps/web/src/server/email-templates/components/EmailButton.tsx
  • apps/web/src/server/email-templates/components/EmailFooter.tsx
  • apps/web/src/server/email-templates/index.ts
  • apps/web/src/server/email-templates/OtpEmail.tsx
  • apps/web/src/server/email-templates/components/EmailHeader.tsx
  • apps/web/src/server/email-templates/TeamInviteEmail.tsx
**/*.{js,jsx,ts,tsx}

📄 CodeRabbit Inference Engine (.cursor/rules/general.mdc)

Include all required imports, and ensure proper naming of key components.

Files:

  • apps/web/src/server/email-templates/test.ts
  • apps/web/src/app/api/dev/email-preview/route.ts
  • apps/web/src/server/email-templates/components/EmailLayout.tsx
  • apps/web/src/server/mailer.ts
  • apps/web/src/server/email-templates/components/EmailButton.tsx
  • apps/web/src/server/email-templates/components/EmailFooter.tsx
  • apps/web/src/server/email-templates/index.ts
  • apps/web/src/server/email-templates/OtpEmail.tsx
  • apps/web/src/server/email-templates/components/EmailHeader.tsx
  • apps/web/src/server/email-templates/TeamInviteEmail.tsx
{apps,packages}/**/*.{jsx,tsx}

📄 CodeRabbit Inference Engine (CLAUDE.md)

{apps,packages}/**/*.{jsx,tsx}: Use functional React components with hooks and group related hooks together
In React components, structure code with props at the top, hooks next, helper functions, then JSX

Files:

  • apps/web/src/server/email-templates/components/EmailLayout.tsx
  • apps/web/src/server/email-templates/components/EmailButton.tsx
  • apps/web/src/server/email-templates/components/EmailFooter.tsx
  • apps/web/src/server/email-templates/OtpEmail.tsx
  • apps/web/src/server/email-templates/components/EmailHeader.tsx
  • apps/web/src/server/email-templates/TeamInviteEmail.tsx
🧬 Code Graph Analysis (5)
apps/web/src/server/email-templates/test.ts (3)
apps/web/src/server/email-templates/OtpEmail.tsx (1)
  • renderOtpEmail (101-103)
apps/web/src/server/email-templates/index.ts (2)
  • renderOtpEmail (1-1)
  • renderTeamInviteEmail (2-2)
apps/web/src/server/email-templates/TeamInviteEmail.tsx (1)
  • renderTeamInviteEmail (84-86)
apps/web/src/app/api/dev/email-preview/route.ts (2)
apps/web/src/server/email-templates/OtpEmail.tsx (1)
  • renderOtpEmail (101-103)
apps/web/src/server/email-templates/TeamInviteEmail.tsx (1)
  • renderTeamInviteEmail (84-86)
apps/web/src/server/mailer.ts (3)
apps/web/src/server/email-templates/OtpEmail.tsx (1)
  • renderOtpEmail (101-103)
apps/web/src/server/email-templates/index.ts (2)
  • renderOtpEmail (1-1)
  • renderTeamInviteEmail (2-2)
apps/web/src/server/email-templates/TeamInviteEmail.tsx (1)
  • renderTeamInviteEmail (84-86)
apps/web/src/server/email-templates/OtpEmail.tsx (6)
apps/web/src/server/email-templates/index.ts (2)
  • OtpEmail (1-1)
  • renderOtpEmail (1-1)
apps/web/src/server/email-templates/components/EmailLayout.tsx (1)
  • EmailLayout (16-77)
apps/web/src/server/email-templates/components/EmailHeader.tsx (1)
  • EmailHeader (9-45)
apps/web/src/server/email-templates/components/EmailButton.tsx (1)
  • EmailButton (10-38)
apps/web/src/server/email-templates/components/EmailFooter.tsx (1)
  • EmailFooter (9-43)
packages/email-editor/src/renderer.tsx (1)
  • render (196-203)
apps/web/src/server/email-templates/TeamInviteEmail.tsx (6)
apps/web/src/server/email-templates/index.ts (2)
  • TeamInviteEmail (2-2)
  • renderTeamInviteEmail (2-2)
apps/web/src/server/email-templates/components/EmailLayout.tsx (1)
  • EmailLayout (16-77)
apps/web/src/server/email-templates/components/EmailHeader.tsx (1)
  • EmailHeader (9-45)
apps/web/src/server/email-templates/components/EmailButton.tsx (1)
  • EmailButton (10-38)
apps/web/src/server/email-templates/components/EmailFooter.tsx (1)
  • EmailFooter (9-43)
packages/email-editor/src/renderer.tsx (1)
  • render (196-203)
🪛 ast-grep (0.38.6)
apps/web/src/server/email-templates/components/EmailLayout.tsx

[warning] 30-30: Usage of dangerouslySetInnerHTML detected. This bypasses React's built-in XSS protection. Always sanitize HTML content using libraries like DOMPurify before injecting it into the DOM to prevent XSS attacks.
Context: dangerouslySetInnerHTML
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml
- https://cwe.mitre.org/data/definitions/79.html

(react-unsafe-html-injection)

🪛 Biome (2.1.2)
apps/web/src/server/email-templates/components/EmailLayout.tsx

[error] 31-31: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)

🔇 Additional comments (11)
apps/web/package.json (1)

39-39: Peer dependencies are compatible with React 19 and Next.js 15

All peer dependencies for jsx-email@2.7.1 support React “^18.2.0 || ^19”, which aligns with Next 15’s React 19 requirement.

• react: "^18.2.0 || ^19"
• react-dom: "^18.2.0 || ^19"
• @jsx-email/plugin-inline: "^1.0.1"
• @jsx-email/plugin-minify: "^1.0.2"
• @jsx-email/plugin-pretty: "^1.0.0"

Ensure you install any of the @jsx-email plugins you intend to use to satisfy those peer dependencies.

apps/web/src/server/email-templates/components/EmailHeader.tsx (1)

2-2: Optional: Alphabetize named imports to match repo guidelines.

Minor consistency tweak.

-import { Container, Heading, Img } from "jsx-email";
+import { Container, Heading, Img } from "jsx-email"; // Already alphabetical; keep as-is
apps/web/src/server/email-templates/test.ts (1)

18-21: No changes needed: optional props

The TeamInviteEmailProps interface marks both inviterName and role as optional, so the test’s use of only teamName and inviteUrl is valid.

apps/web/src/server/mailer.ts (4)

33-39: LGTM: switched OTP email HTML to the new template renderer

Asynchronously rendering via jsx-email is clean and keeps the HTML concern in templates.


35-35: Confirm OTP casing behavior (uppercase in HTML vs. text/plain)

HTML uses token.toUpperCase() while the text body uses the original token. If OTP comparison is case-sensitive, this can cause user confusion or failed sign-ins.

If OTP is case-sensitive, align both representations. Example change to keep original casing:

   const html = await renderOtpEmail({
-    otpCode: token.toUpperCase(),
+    otpCode: token,
     loginUrl: url,
     hostName: host,
   });

Alternatively, uppercase both HTML and text if your backend normalizes input to uppercase.


61-65: LGTM: team invite HTML now uses the template renderer

Good separation of concerns and consistency with OTP emails.


61-64: TeamInviteEmailProps usage is correct

The TeamInviteEmailProps interface (in apps/web/src/server/email-templates/TeamInviteEmail.tsx) defines:

  • teamName: string
  • inviteUrl: string
  • inviterName?: string

The mailer call in apps/web/src/server/mailer.ts passes only the required fields (teamName and inviteUrl), which is valid—the optional inviterName will simply be undefined in the template. There is no role prop on the interface, so the review’s reference to a “role” field is outdated.

If you’d like richer emails, you can pass inviterName (and extend the interface/template to include a role if needed) wherever you have that data; otherwise, no changes are required here.

apps/web/src/server/email-templates/OtpEmail.tsx (1)

96-103: LGTM: renderer helper keeps callers simple

The renderOtpEmail helper is concise and matches how other templates are exposed.

apps/web/src/server/email-templates/index.ts (1)

1-2: Good centralization of template exports

Barrel exports make consumers cleaner and promote consistency.

apps/web/src/server/email-templates/TeamInviteEmail.tsx (2)

17-81: Overall: clean, accessible, and reusable template.

Solid structure with EmailLayout/Header/Footer/Button, preview text set, clear CTA, and responsive-friendly inline styles. The conditional “as a {role}” is a nice touch.


61-63: Ensure inviteUrl is fully qualified

The JSX template (<EmailButton href={inviteUrl}>…) and the server code in sendTeamInviteEmail (which does new URL(url)) both require a complete URL. Please verify that every call to sendTeamInviteEmail constructs and passes an absolute URL (e.g. https://app.yourdomain.com/join-team?inviteId=…). If you can’t guarantee this upstream, normalize it using your app’s origin (for example via env.APP_ORIGIN):

// normalize in your invite-generation logic or here in mailer.ts
inviteUrl: new URL(url, env.APP_ORIGIN).toString(),

What to check:

  • The controller/service that calls sendTeamInviteEmail—ensure it uses a full origin + path
  • Any helper or constant (e.g. env.APP_ORIGIN) responsible for building invitation URLs

No change is needed in the template if callers already pass a fully qualified URL.

Comment thread apps/web/package.json Outdated
"type": "module",
"scripts": {
"dev": "next dev -p 3000",
"dev": "next dev",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Verify there are no hard-coded localhost:3000 assumptions before removing the explicit dev port.

Dropping the explicit port is fine, but if docs, scripts, or preview routes rely on http://localhost:3000, they’ll break. Please verify.

Run this to find any hard-coded references to port 3000:


🏁 Script executed:

#!/bin/bash
# Search for hard-coded dev ports in code, envs, and docs
rg -n -C2 -S -g '!**/node_modules/**' -P '(localhost|127\.0\.0\.1)\s*:\s*3000|http://localhost/|http://127\.0\.0\.1/'

Length of output: 5722


Update hard-coded localhost:3000 references before dropping the explicit dev port

Several docs and scripts still assume http://localhost:3000. These will break once you rely on the default Next.js port or a custom PORT. Please update or parameterize the following locations:

  • docker/README.md (line 27): “Access the Unsend application by visiting http://localhost:3000
  • apps/marketing/README.md (line 17): “Open http://localhost:3000
  • apps/marketing/src/app/editor/page.tsx (lines 15–17): fetch("http://localhost:3000/api/to-html", …)
  • apps/web/src/app/(dashboard)/domains/[domainId]/send-test-mail.tsx (lines 36, 45, 67, 84): multiple examples (JS fetch, Python, Ruby, PHP) pointing at http://localhost:3000/api/v1/emails
  • apps/docs/get-started/local.mdx (lines 115, 121, 159, 171, 201): login, OAuth callback, dashboard URLs and cloudflared tunnel commands all reference port 3000
  • apps/docs/get-started/set-up-docker.mdx (line 28): “visit http://localhost:3000
  • CONTRIBUTION.md (lines 61–62, 92–93, 98): GitHub OAuth homepage/callback URLs and tunnel commands

Recommend replacing hard-coded URLs with http://localhost:$PORT (or an env var), or updating instructions to match the new default.

🤖 Prompt for AI Agents
In apps/web/package.json around line 7 and across the repository
(docker/README.md line 27; apps/marketing/README.md line 17;
apps/marketing/src/app/editor/page.tsx lines 15–17;
apps/web/src/app/(dashboard)/domains/[domainId]/send-test-mail.tsx lines
36,45,67,84; apps/docs/get-started/local.mdx lines 115,121,159,171,201;
apps/docs/get-started/set-up-docker.mdx line 28; CONTRIBUTION.md lines
61–62,92–93,98) replace hard-coded http://localhost:3000 occurrences with a
parameterized form so the port is not fixed: update docs and README text to
reference http://localhost:$PORT or instruct using an env var (e.g., PORT) and
change code examples to interpolate or read process.env.PORT (or explain using
default Next.js port) so fetch/API examples and tunnel commands use the
configurable port; ensure examples include a fallback (e.g., 3000) and update
any shell commands to use $PORT or explicit instruction to set PORT before
running.

Comment on lines +4 to +10
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const type = searchParams.get("type") || "otp";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Restrict the dev preview route to development-only

This is a dev-only utility; gate it by NODE_ENV to avoid exposing it in production.

Apply this diff:

 export async function GET(request: NextRequest) {
+  // Dev-only endpoint: return 404 in non-development environments
+  if (process.env.NODE_ENV !== "development") {
+    return NextResponse.json({ error: "Not Found" }, { status: 404 });
+  }
   const { searchParams } = new URL(request.url);
   const type = searchParams.get("type") || "otp";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const type = searchParams.get("type") || "otp";
export async function GET(request: NextRequest) {
// Dev-only endpoint: return 404 in non-development environments
if (process.env.NODE_ENV !== "development") {
return NextResponse.json({ error: "Not Found" }, { status: 404 });
}
const { searchParams } = new URL(request.url);
const type = searchParams.get("type") || "otp";
// …rest of your logic
}
🤖 Prompt for AI Agents
In apps/web/src/app/api/dev/email-preview/route.ts around lines 4–7, the
dev-only email preview route is currently exposed unconditionally; add a
NODE_ENV check at the start of the GET handler so it only runs in development
(process.env.NODE_ENV === 'development'), and immediately return a 404/403
response (or throw) when not in development to prevent exposure in production;
keep the rest of the handler unchanged and ensure the guard executes before
creating the URL/searchParams or reading any request data.

Comment on lines +30 to +51
<style
dangerouslySetInnerHTML={{
__html: `
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
line-height: 1.6;
color: #374151;
background-color: #f9fafb;
}
.email-container {
max-width: 600px;
margin: 0 auto;
background-color: #ffffff;
}
`,
}}
/>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Address linter error: remove dangerouslySetInnerHTML usage in <style>.

Static analysis flags this as an error; although the CSS is static and safe, the rule will fail CI. Use a normal style child string instead.

-        <style
-          dangerouslySetInnerHTML={{
-            __html: `
-              * {
-                margin: 0;
-                padding: 0;
-                box-sizing: border-box;
-              }
-              body {
-                font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
-                line-height: 1.6;
-                color: #374151;
-                background-color: #f9fafb;
-              }
-              .email-container {
-                max-width: 600px;
-                margin: 0 auto;
-                background-color: #ffffff;
-              }
-            `,
-          }}
-        />
+        <style>{`
+          * {
+            margin: 0;
+            padding: 0;
+            box-sizing: border-box;
+          }
+          body {
+            font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+            line-height: 1.6;
+            color: #374151;
+            background-color: #f9fafb;
+          }
+          .email-container {
+            max-width: 600px;
+            margin: 0 auto;
+            background-color: #ffffff;
+          }
+        `}</style>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<style
dangerouslySetInnerHTML={{
__html: `
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
line-height: 1.6;
color: #374151;
background-color: #f9fafb;
}
.email-container {
max-width: 600px;
margin: 0 auto;
background-color: #ffffff;
}
`,
}}
/>
<style>{`
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
line-height: 1.6;
color: #374151;
background-color: #f9fafb;
}
.email-container {
max-width: 600px;
margin: 0 auto;
background-color: #ffffff;
}
`}</style>
🧰 Tools
🪛 ast-grep (0.38.6)

[warning] 30-30: Usage of dangerouslySetInnerHTML detected. This bypasses React's built-in XSS protection. Always sanitize HTML content using libraries like DOMPurify before injecting it into the DOM to prevent XSS attacks.
Context: dangerouslySetInnerHTML
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml
- https://cwe.mitre.org/data/definitions/79.html

(react-unsafe-html-injection)

🪛 Biome (2.1.2)

[error] 31-31: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)

🤖 Prompt for AI Agents
In apps/web/src/server/email-templates/components/EmailLayout.tsx around lines
30 to 51, remove the dangerouslySetInnerHTML usage on the <style> tag and
replace it with a normal child string so the linter rule no longer flags it;
specifically, delete the dangerouslySetInnerHTML prop and place the same CSS as
the element's child using a JSX expression (e.g. <style>{`...css...`}</style>),
preserving the exact CSS content and formatting.

Comment thread apps/web/src/server/email-templates/TeamInviteEmail.tsx Outdated
KMKoushik and others added 7 commits August 17, 2025 09:10
- Add minimal, professional email templates using jsx-email
- Create reusable email components (layout, header, footer, button)
- Implement OTP email with prominent code display and one-click login
- Implement team invite email with feature highlights and clear CTA
- Update mailer service to use new jsx-email templates
- Add development email preview endpoint for testing
- Ensure mobile-responsive design with accessibility features

🤖 Generated with [opencode](https://opencode.ai)

Co-Authored-By: opencode <noreply@opencode.ai>
- Change buttons to black background with white text
- Remove colored elements, use only black and white styling
- Change text alignment to left instead of center
- Remove "This code will expire in 10 minutes" from OTP email
- Remove team features list and team card from invite email
- Simplify invite email to just show invitation text and accept button
- Use minimal border radius and remove shadows for cleaner look

🤖 Generated with [opencode](https://opencode.ai)

Co-Authored-By: opencode <noreply@opencode.ai>
- Remove all borders and card-like styling from email containers
- Remove border radius and shadows for plain appearance
- Make OTP code display simple with just light background
- Reduce padding for normal email spacing
- Remove horizontal rules/separators
- Left-align all text including footer
- Make emails look like standard plain text emails with buttons

🤖 Generated with [opencode](https://opencode.ai)

Co-Authored-By: opencode <noreply@opencode.ai>
- Set OTP code container and text to left alignment
- Set button containers to left alignment
- Change button text alignment from center to left
- Ensure all elements follow left-aligned layout consistently

🤖 Generated with [opencode](https://opencode.ai)

Co-Authored-By: opencode <noreply@opencode.ai>
- Remove margin: 0 auto from main container that was centering content
- Add textAlign: left to all Container components
- Add textAlign: left to all Text components
- Fix logo alignment from center to left in header
- Add textAlign: left to heading in header
- Ensure jsx-email Container components don't use default centering
- Make all content consistently left-aligned throughout emails

🤖 Generated with [opencode](https://opencode.ai)

Co-Authored-By: opencode <noreply@opencode.ai>
- Replace jsx-email Link component with native anchor tag
- Add float: left and clear: both to force left positioning
- Keep textAlign: center for button text itself (normal for buttons)
- Ensure button appears at left edge instead of centering

🤖 Generated with [opencode](https://opencode.ai)

Co-Authored-By: opencode <noreply@opencode.ai>
@KMKoushik KMKoushik force-pushed the feat/jsx-email-templates branch from fbdcc31 to 270c115 Compare August 17, 2025 03:09
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
apps/web/src/server/email-templates/components/EmailLayout.tsx (1)

30-51: Fix linter error: replace dangerouslySetInnerHTML on <style> with string child

This is blocking CI per Biome and ast-grep. The CSS is static; render it as a normal child string.

Apply this diff:

-        <style
-          dangerouslySetInnerHTML={{
-            __html: `
-              * {
-                margin: 0;
-                padding: 0;
-                box-sizing: border-box;
-              }
-              body {
-                font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
-                line-height: 1.6;
-                color: #374151;
-                background-color: #f9fafb;
-              }
-              .email-container {
-                max-width: 600px;
-                margin: 0 auto;
-                background-color: #ffffff;
-              }
-            `,
-          }}
-        />
+        <style>{`
+          * {
+            margin: 0;
+            padding: 0;
+            box-sizing: border-box;
+          }
+          body {
+            font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+            line-height: 1.6;
+            color: #374151;
+            background-color: #f9fafb;
+          }
+          .email-container {
+            max-width: 600px;
+            margin: 0 auto;
+            background-color: #ffffff;
+          }
+        `}</style>
🧹 Nitpick comments (3)
apps/web/src/server/email-templates/components/EmailLayout.tsx (3)

39-43: Resolve background-color mismatch between CSS and style

CSS sets body to #f9fafb while the inline style sets #ffffff. Align them to avoid surprises across clients.

If the intent is a light gray page background with a white container, update :

-      <Body style={{ backgroundColor: "#ffffff", padding: "20px" }}>
+      <Body style={{ backgroundColor: "#f9fafb", padding: "20px" }}>

If you prefer a fully white page, instead remove the background-color from the body rule in the CSS block.

Also applies to: 63-63


64-71: Inline centering and width for better email client support

Many clients ignore class-based CSS; inlining width and centering improves reliability and responsiveness.

Apply this diff:

         <Container
           className="email-container"
           style={{
+            width: "100%",
             maxWidth: "600px",
+            margin: "0 auto",
             backgroundColor: "#ffffff",
-            textAlign: "left" as const,
+            textAlign: "left",
           }}
         >

18-19: Add lang attribute to for accessibility and better client hints

This helps screen readers and some clients render text appropriately.

-    <Html>
+    <Html lang="en">
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between fbdcc31 and 270c115.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (11)
  • apps/web/package.json (1 hunks)
  • apps/web/src/app/api/dev/email-preview/route.ts (1 hunks)
  • apps/web/src/server/email-templates/OtpEmail.tsx (1 hunks)
  • apps/web/src/server/email-templates/TeamInviteEmail.tsx (1 hunks)
  • apps/web/src/server/email-templates/components/EmailButton.tsx (1 hunks)
  • apps/web/src/server/email-templates/components/EmailFooter.tsx (1 hunks)
  • apps/web/src/server/email-templates/components/EmailHeader.tsx (1 hunks)
  • apps/web/src/server/email-templates/components/EmailLayout.tsx (1 hunks)
  • apps/web/src/server/email-templates/index.ts (1 hunks)
  • apps/web/src/server/email-templates/test.ts (1 hunks)
  • apps/web/src/server/mailer.ts (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (10)
  • apps/web/src/server/email-templates/index.ts
  • apps/web/src/app/api/dev/email-preview/route.ts
  • apps/web/src/server/email-templates/components/EmailHeader.tsx
  • apps/web/src/server/email-templates/components/EmailButton.tsx
  • apps/web/src/server/email-templates/TeamInviteEmail.tsx
  • apps/web/src/server/email-templates/OtpEmail.tsx
  • apps/web/src/server/mailer.ts
  • apps/web/package.json
  • apps/web/src/server/email-templates/components/EmailFooter.tsx
  • apps/web/src/server/email-templates/test.ts
🧰 Additional context used
📓 Path-based instructions (6)
{apps,packages}/**/*.{js,jsx,ts,tsx,css,scss,md,mdx}

📄 CodeRabbit Inference Engine (CLAUDE.md)

Use Prettier with the Tailwind plugin for code formatting

Files:

  • apps/web/src/server/email-templates/components/EmailLayout.tsx
{apps,packages}/**/*.{js,jsx,ts,tsx}

📄 CodeRabbit Inference Engine (CLAUDE.md)

{apps,packages}/**/*.{js,jsx,ts,tsx}: Group imports by source (internal/external) and alphabetize them
Use camelCase for variables and functions, PascalCase for components and classes
Use try/catch with specific error types for error handling

Files:

  • apps/web/src/server/email-templates/components/EmailLayout.tsx
{apps,packages}/**/*.{ts,tsx}

📄 CodeRabbit Inference Engine (CLAUDE.md)

{apps,packages}/**/*.{ts,tsx}: Use strong typing in TypeScript, avoid any, and use Zod for validation
Follow Vercel style guides with strict TypeScript

Files:

  • apps/web/src/server/email-templates/components/EmailLayout.tsx
{apps,packages}/**/*.{jsx,tsx}

📄 CodeRabbit Inference Engine (CLAUDE.md)

{apps,packages}/**/*.{jsx,tsx}: Use functional React components with hooks and group related hooks together
In React components, structure code with props at the top, hooks next, helper functions, then JSX

Files:

  • apps/web/src/server/email-templates/components/EmailLayout.tsx
apps/web/**/*.{ts,tsx}

📄 CodeRabbit Inference Engine (CLAUDE.md)

Use tRPC for internal API endpoints

Files:

  • apps/web/src/server/email-templates/components/EmailLayout.tsx
**/*.{js,jsx,ts,tsx}

📄 CodeRabbit Inference Engine (.cursor/rules/general.mdc)

Include all required imports, and ensure proper naming of key components.

Files:

  • apps/web/src/server/email-templates/components/EmailLayout.tsx
🪛 ast-grep (0.38.6)
apps/web/src/server/email-templates/components/EmailLayout.tsx

[warning] 30-30: Usage of dangerouslySetInnerHTML detected. This bypasses React's built-in XSS protection. Always sanitize HTML content using libraries like DOMPurify before injecting it into the DOM to prevent XSS attacks.
Context: dangerouslySetInnerHTML
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml
- https://cwe.mitre.org/data/definitions/79.html

(react-unsafe-html-injection)

🪛 Biome (2.1.2)
apps/web/src/server/email-templates/components/EmailLayout.tsx

[error] 31-31: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)

🔇 Additional comments (1)
apps/web/src/server/email-templates/components/EmailLayout.tsx (1)

52-61: Good meta coverage for email clients

Nice inclusion of viewport, format-detection, color-scheme, and Apple reformatting metas. The Preview placement is also spot on.

@KMKoushik KMKoushik merged commit 9128687 into main Aug 17, 2025
4 checks passed
@KMKoushik KMKoushik deleted the feat/jsx-email-templates branch August 17, 2025 03:17
@coderabbitai coderabbitai bot mentioned this pull request Aug 24, 2025
This was referenced Aug 31, 2025
@coderabbitai coderabbitai bot mentioned this pull request Dec 19, 2025
10 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant