Novu 仓库 Agent 技能实战:基于 React Email 与 Tailwind 的通用邮件模板模式详解
2026/9/10 16:42:53 网站建设 项目流程

Novu 仓库 Agent 技能实战:基于 React Email 与 Tailwind 的通用邮件模板模式详解

【免费下载链接】novuThe open-source communication infrastructure for agents and products项目地址: https://gitcode.com/GitHub_Trending/no/novu

本技能文档沉淀于本仓库.agents/skills/react-email/目录,供研发 Agent 在编写 HTML 邮件模板时直接引用。它以「真实业务模板」为骨架,给出密码重置、订单确认、通知告警、多栏 Newsletter、团队邀请五类高频场景的完整 React Email + Tailwind CSS 实现,并内置了pixelBasedPreset像素化预设、TypeScript 类型、PreviewProps预览数据与响应式布局等工程约定。读完本文,你既能直接复制改造出可运行的邮件组件,也能理解 React Email 在「一次编写、多邮件客户端兼容」前提下做复杂版式的核心写法。

一、本文档的定位与所在仓库背景

该技能文档位于仓库.agents/skills/react-email/references/PATTERNS.md,是其父技能 .agents/skills/react-email/SKILL.md 的附属参考材料。SKILL.md 描述了技能启用范围:当用户需要"用 React 组件编写 HTML 邮件模板——欢迎邮件、密码重置、通知、订单确认、Newsletter 或事务邮件"时调用本技能。配套参考还包括完整组件参考 references/COMPONENTS.md、样式指南 references/STYLING.md、发送指南 references/SENDING.md 与国际化指南 references/I18N.md。

本文档本身不讲述组件 API,而是直接给出 5 个端到端可运行的完整模板文件,回答一个现实问题:真实产品里最常见的邮件到底该怎么写。它同时承担着向 Agent 输出"标准答案"的角色,因此每个示例都刻意做到:

  • 全部样式走 Tailwind CSS 工具类,且必须传入pixelBasedPreset预设;
  • 为每个模板定义严格的 TypeScriptProps接口;
  • 每个模板附带.PreviewProps静态属性,用于本地预览与开发调试;
  • 覆盖多栏布局、列表渲染、代码高亮、CTA 按钮、退订链接等复杂场景。

二、五个示例背后的公共约定

在逐个分析模板前,先提炼它们在代码结构上的共性。理解这些约定后,阅读甚至自行编写任何一封 React Email 邮件都会更顺畅。这些约定与 SKILL.md 中的"Styling considerations"和 references/STYLING.md 一脉相承:

  1. 根结构固定为<Html lang="en"><Tailwind config={{ presets: [pixelBasedPreset] }}><Head /><Body><Container>。其中<Head />在启用 Tailwind 时必须放在<Tailwind>内部。
  2. 为何必须使用pixelBasedPreset:绝大多数邮件客户端不支持rem单位。pixelBasedPreset会将 Tailwind 基于 rem 的工具类换算为像素值。这是 React Email + Tailwind 写邮件的头号硬性约束,五个示例无一例外全部传入该预设。
  3. 布局禁用 flexbox/grid:邮件客户端大多不解析 flex/grid,复杂布局必须用Section+Row+Column组成的表格式结构(Order Confirmation 与 Newsletter 示例是典型代表)。
  4. 预览文本<Preview>始终紧跟<Head />之后、作为<Body>内第一个语义内容,控制收件箱列表里显示的那一行摘要。
  5. 通过静态属性.PreviewProps提供演示数据,方便本地 dev server 直接渲染查看,无需真实数据源。
  6. 禁用媒体查询(sm:/md:等)、dark:主题选择器、SVG/WEBP 图片;文案中的日期等动态值用 JS 内置 API 格式化。

三、密码重置邮件:单卡片 + 单一 CTA 的范式

密码重置是事务邮件的入门标准件:头部标题 + 说明文字 + 唯一行动按钮 + 安全提示脚注。React Email 组件版见下(完整代码即文档原始实现):

import { Html, Head, Preview, Body, Container, Heading, Text, Button, Hr, Tailwind, pixelBasedPreset } from '@react-email/components'; interface PasswordResetProps { resetUrl: string; email: string; expiryHours?: number; } export default function PasswordReset({ resetUrl, email, expiryHours = 1 }: PasswordResetProps) { return ( <Html lang="en"> <Tailwind config={{ presets: [pixelBasedPreset] }}> <Head /> <Preview>Reset your password - Action required</Preview> <Body className="bg-gray-100 font-sans"> <Container className="mx-auto py-10 px-5 max-w-xl bg-white"> <Heading className="text-2xl font-bold text-gray-800 mb-5"> Reset Your Password </Heading> <Text className="text-base leading-7 text-gray-800 my-4"> A password reset was requested for your account: <strong>{email}</strong> </Text> <Text className="text-base leading-7 text-gray-800 my-4"> Click the button below to reset your password. This link expires in {expiryHours} hour{expiryHours > 1 ? 's' : ''}. </Text> <Button href={resetUrl} className="bg-red-600 text-white px-7 py-3.5 rounded block text-center font-bold my-6 no-underline" > Reset Password </Button> <Hr className="border-gray-200 my-6" /> <Text className="text-sm text-gray-500 leading-5 my-2"> If you didn't request this, please ignore this email. Your password will remain unchanged. </Text> <Text className="text-sm text-gray-500 leading-5 my-2"> For security, this link will only work once. </Text> </Container> </Body> </Tailwind> </Html> ); } PasswordReset.PreviewProps = { resetUrl: 'https://example.com/reset/abc123', email: 'user@example.com', expiryHours: 1 } as PasswordResetProps;

该示例值得学习的写法

  • Props 默认值下沉到组件签名expiryHours = 1让调用方可以不传有效期;复数处理用内联三元hour{expiryHours > 1 ? 's' : ''},避免引入额外的复数库。
  • 按钮块级化Button的 className 使用block text-center,让整行都可点击、文字居中,同时no-underline去除超链接下划线。按 references/COMPONENTS.md 的说明,Button本身已内置 Outlook 内边距问题的工作区。
  • 字号体系分三层:主标题text-2xl font-bold→ 正文text-base leading-7→ 提示文字text-sm text-gray-500,通过字号、字重、颜色三层差异建立视觉层级。
  • .PreviewPropsas断言:以} as PasswordResetProps结束,保证静态属性里的示例数据与接口严格一致(示例中resetUrl等均为演示占位 URL)。
  • 安全话术是事务邮件刚需:用<Hr />把正文与免责提示分隔,第二段提示"该链接仅可使用一次",属于密码重置场景的标配安全文案。

四、订单确认邮件:Row/Column 双栏版式与金额表格

订单确认的核心难点是逐行商品列表金额小计/运费/税费/合计的右对齐表格,这要求在两列(乃至三列)版式下精确对齐。由于邮件客户端不支持 flex,React Email 用Row+Column以表格语义实现。文档原始实现如下:

import { Html, Head, Preview, Body, Container, Section, Row, Column, Heading, Text, Img, Hr, Tailwind, pixelBasedPreset } from '@react-email/components'; interface Product { name: string; price: number; quantity: number; image: string; sku?: string; } interface OrderConfirmationProps { orderNumber: string; orderDate: Date; items: Product[]; subtotal: number; shipping: number; tax: number; total: number; shippingAddress: { name: string; street: string; city: string; state: string; zip: string; country: string; }; } export default function OrderConfirmation({ orderNumber, orderDate, items, subtotal, shipping, tax, total, shippingAddress }: OrderConfirmationProps) { return ( <Html lang="en"> <Tailwind config={{ presets: [pixelBasedPreset] }}> <Head /> <Preview>Order #{orderNumber} confirmed - Thank you for your purchase!</Preview> <Body className="bg-gray-100 font-sans"> <Container className="mx-auto py-10 px-5 max-w-xl"> <Heading className="text-3xl font-bold text-gray-800 mb-2"> Order Confirmed </Heading> <Text className="text-base text-gray-500 mb-6">Thank you for your order!</Text> <Section className="bg-gray-50 p-4 rounded mb-6"> <Row> <Column> <Text className="text-xs text-gray-500 uppercase mb-1">Order Number</Text> <Text className="text-base font-bold text-gray-800 m-0">#{orderNumber}</Text> </Column> <Column> <Text className="text-xs text-gray-500 uppercase mb-1">Order Date</Text> <Text className="text-base font-bold text-gray-800 m-0">{orderDate.toLocaleDateString()}</Text> </Column> </Row> </Section> <Hr className="border-gray-200 my-6" /> <Heading as="h2" className="text-xl font-bold text-gray-800 my-4"> Order Items </Heading> {items.map((item, index) => ( <Section key={index} className="mb-4"> <Row> <Column className="w-20 align-top"> <Img src={item.image} alt={item.name} width="80" height="80" className="rounded border border-gray-200" /> </Column> <Column className="align-top pl-4"> <Text className="text-base font-bold text-gray-800 m-0 mb-1">{item.name}</Text> {item.sku && <Text className="text-sm text-gray-400 m-0 mb-2">SKU: {item.sku}</Text>} <Text className="text-sm text-gray-500 m-0"> Quantity: {item.quantity} × ${item.price.toFixed(2)} </Text> </Column> <Column className="w-24 text-right align-top"> <Text className="text-base font-bold text-gray-800 m-0"> ${(item.quantity * item.price).toFixed(2)} </Text> </Column> </Row> </Section> ))} <Hr className="border-gray-200 my-6" /> <Section className="mt-6"> <Row> <Column><Text className="text-sm text-gray-500 my-2">Subtotal</Text></Column> <Column className="text-right"> <Text className="text-sm text-gray-800 my-2">${subtotal.toFixed(2)}</Text> </Column> </Row> <Row> <Column><Text className="text-sm text-gray-500 my-2">Shipping</Text></Column> <Column className="text-right"> <Text className="text-sm text-gray-800 my-2">${shipping.toFixed(2)}</Text> </Column> </Row> <Row> <Column><Text className="text-sm text-gray-500 my-2">Tax</Text></Column> <Column className="text-right"> <Text className="text-sm text-gray-800 my-2">${tax.toFixed(2)}</Text> </Column> </Row> <Hr className="border-gray-200 my-3" /> <Row> <Column><Text className="text-lg font-bold text-gray-800 my-2">Total</Text></Column> <Column className="text-right"> <Text className="text-lg font-bold text-gray-800 my-2">${total.toFixed(2)}</Text> </Column> </Row> </Section> <Hr className="border-gray-200 my-6" /> <Heading as="h2" className="text-xl font-bold text-gray-800 my-4"> Shipping Address </Heading> <Section className="bg-gray-50 p-4 rounded"> <Text className="text-sm text-gray-800 my-1">{shippingAddress.name}</Text> <Text className="text-sm text-gray-800 my-1">{shippingAddress.street}</Text> <Text className="text-sm text-gray-800 my-1"> {shippingAddress.city}, {shippingAddress.state} {shippingAddress.zip} </Text> <Text className="text-sm text-gray-800 my-1">{shippingAddress.country}</Text> </Section> <Text className="text-sm text-gray-500 mt-8"> Questions about your order? Reply to this email and we'll help you out. </Text> </Container> </Body> </Tailwind> </Html> ); } OrderConfirmation.PreviewProps = { orderNumber: '10234', orderDate: new Date(), items: [ { name: 'Vintage Macintosh', price: 499.00, quantity: 1, image: 'https://via.placeholder.com/80', sku: 'MAC-001' }, { name: 'Mechanical Keyboard', price: 149.99, quantity: 2, image: 'https://via.placeholder.com/80', sku: 'KEY-042' } ], subtotal: 798.98, shipping: 15.00, tax: 69.42, total: 883.40, shippingAddress: { name: 'John Doe', street: '123 Main St', city: 'San Francisco', state: 'CA', zip: '94102', country: 'USA' } } as OrderConfirmationProps;

该示例值得学习的写法

  • 商品卡片的行结构:每个items.map项生成一个Section > Row,内部为图片栏(w-20 align-top)、信息栏(align-top pl-4)、右侧金额栏(w-24 text-right align-top)三Column布局。Column必须包裹在Row内使用(见 COMPONENTS.md 的组件约束)。
  • 宽度用百分比/固定像素组合:左列固定w-20(80px),右列固定w-24(96px),中列自适应——比全部百分比更贴合图文混排的真实需求。align-top防止表格单元格默认垂直居中导致的三栏顶部错位。
  • 金额渲染纪律:所有金额一律用.toFixed(2)保证两位小数输出;右侧栏统一text-right,与左侧文本右边界严格对齐,避免小数点错位。
  • 数据接口扁平化与嵌套并用:顶层 props 扁平展开金额字段,shippingAddress用嵌套对象收纳地址,sku设计为可选(sku?: string),用{item.sku && ...}条件渲染,保证无 SKU 商品也能正常展示。
  • 灰度卡片制造焦点:订单号/日期信息块使用bg-gray-50 p-4 rounded的浅灰底,与白底主卡片区分,符合"次要信息弱化"的排版原则;<Preview>中的动态值Order #{orderNumber}直接拼入预览摘要,提升打开率。

五、通知告警邮件:CodeBlock 代码块与语义化严重级别配色

DevOps/SRE 场景中,"部署失败/构建报错"类通知需要在邮件正文中贴出日志。React Email 提供基于 Prism.js 的CodeBlock组件(可选主题与行号),配合 severity 驱动的动态配色可以做出专业告警模板。文档原始实现如下:

import { Html, Head, Preview, Body, Container, Section, Heading, Text, CodeBlock, dracula, Hr, Link, Tailwind, pixelBasedPreset } from '@react-email/components'; interface NotificationProps { title: string; message: string; severity: 'info' | 'warning' | 'error' | 'success'; timestamp: Date; logData?: string; actionUrl?: string; actionLabel?: string; } export default function Notification({ title, message, severity, timestamp, logData, actionUrl, actionLabel = 'View Details' }: NotificationProps) { const severityColors = { info: 'bg-sky-500', warning: 'bg-amber-500', error: 'bg-red-500', success: 'bg-green-500' }; const severityBtnColors = { info: 'bg-sky-500', warning: 'bg-amber-500', error: 'bg-red-500', success: 'bg-green-500' }; return ( <Html lang="en"> <Tailwind config={{ presets: [pixelBasedPreset] }}> <Head /> <Preview>{title} - {severity}</Preview> <Body className="bg-gray-100 font-mono"> <Container className="mx-auto max-w-xl bg-white border border-gray-200 rounded overflow-hidden"> <Section className={`h-1 w-full ${severityColors[severity]}`} /> <Heading className="text-2xl font-bold text-gray-800 mx-6 mt-6 mb-4"> {title} </Heading> <Text className={`inline-block px-3 py-1 text-xs font-bold text-white rounded-full mx-6 mb-4 ${severityBtnColors[severity]}`}> {severity.toUpperCase()} </Text> <Text className="text-base leading-6 text-gray-800 mx-6 mb-4"> {message} </Text> <Text className="text-sm text-gray-500 mx-6 mb-6"> {new Date(timestamp).toLocaleString('en-US', { dateStyle: 'long', timeStyle: 'short' })} </Text> {logData && ( <> <Hr className="border-gray-200 my-6" /> <Heading as="h2" className="text-lg font-bold text-gray-800 mx-6 my-4"> Log Details </Heading> <Section className="mx-6"> <CodeBlock code={logData} language="json" theme={dracula} lineNumbers /> </Section> </> )} {actionUrl && ( <> <Hr className="border-gray-200 my-6" /> <Link href={actionUrl} className={`inline-block px-6 py-3 text-base font-bold text-white rounded no-underline mx-6 mb-6 ${severityBtnColors[severity]}`} > {actionLabel} </Link> </> )} <Hr className="border-gray-200 my-6" /> <Text className="text-xs text-gray-500 mx-6 mb-6"> This is an automated notification. Please do not reply to this email. </Text> </Container> </Body> </Tailwind> </Html> ); } Notification.PreviewProps = { title: 'Deployment Failed', message: 'The deployment to production environment has failed. Please review the logs and take corrective action.', severity: 'error', timestamp: new Date(), logData: `{ "error": "Build failed", "exit_code": 1, "duration": "2m 34s", "commit": "abc123def" }`, actionUrl: 'https://example.com/deployments/123', actionLabel: 'View Deployment' } as NotificationProps;

该示例值得学习的写法

  • severity 字典映射驱动整封邮件severity被限制为字面量联合类型'info' | 'warning' | 'error' | 'success'。两个颜色字典(顶部色条/按钮与胶囊徽章同色系)把"语义等级"翻译为 Tailwind 类名,保证顶部色条、徽章与操作链接颜色三者一致。注意:作者保留了同色的两套字典变量,便于将来让按钮采用独立强调色。
  • font-mono语境:整封告警邮件的<Body>使用等宽字体族font-mono,与日志内容气质统一。
  • CodeBlock用法code(原始字符串)、language="json"theme={dracula}(从@react-email/components直接导出)、lineNumbers开启行号。按照 COMPONENTS.md 与 SKILL.md 的提示,CodeBlock 所属的Section外层理论上还应配overflow-auto容器防内边距溢出,实践中可视宽度决定取舍。
  • 可选片段用&&组合 JSX{logData && (<>...</>)}{actionUrl && (...)}在内容缺失时自动整段隐藏,配套标签/分隔线也随之消失,保证无日志或无跳转链接时邮件不出现空洞区域。
  • 日期本地化格式化new Date(timestamp).toLocaleString('en-US', { dateStyle: 'long', timeStyle: 'short' })生成如 "September 8, 2026 at 1:45 AM" 的可读时间,是邮件内展示时间戳的推荐做法。
  • 徽章用inline-block+rounded-full:胶囊状 severity 徽章不占用整行,与标题保持在同一视觉区块。

六、多栏 Newsletter:头条 + 双栏文章流 + 页脚退订

Newsletter 是邮件版式复杂度最高的场景之一:页眉 Logo、头条推荐、按行切分的双栏文章卡片、可点击"Read article"、以及必须包含物理地址与退订链接的页脚。以下文档原始实现展示了如何只用 Row/Column(表格)在邮件里还原响应式网格

import { Html, Head, Preview, Body, Container, Section, Row, Column, Heading, Text, Img, Button, Hr, Link, Tailwind, pixelBasedPreset } from '@react-email/components'; interface Article { title: string; excerpt: string; image: string; url: string; author: string; date: string; } interface NewsletterProps { articles: Article[]; unsubscribeUrl: string; } export default function Newsletter({ articles, unsubscribeUrl }: NewsletterProps) { return ( <Html lang="en"> <Tailwind config={{ presets: [pixelBasedPreset] }}> <Head /> <Preview>Your weekly roundup of the latest articles</Preview> <Body className="bg-white font-sans"> <Container className="mx-auto max-w-xl"> {/* Header */} <Section className="pt-10 px-5 pb-5 text-center"> <Img src="https://via.placeholder.com/150x50?text=Logo" alt="Company Logo" width="150" height="50" /> </Section> <Heading className="text-3xl font-bold text-gray-900 mx-5 mb-4 text-center"> This Week's Highlights </Heading> <Text className="text-base leading-6 text-gray-500 mx-5 mb-6 text-center"> Here are the top articles from this week. Enjoy your reading! </Text> <Hr className="border-gray-200 mx-5 my-8" /> {/* Featured Article */} {articles[0] && ( <Section className="px-5"> <Img src={articles[0].image} alt={articles[0].title} width="600" className="w-full rounded-lg mb-4" /> <Heading as="h2" className="text-2xl font-bold text-gray-900 my-4"> {articles[0].title} </Heading> <Text className="text-base leading-6 text-gray-500 my-4"> {articles[0].excerpt} </Text> <Text className="text-sm text-gray-400 my-2"> By {articles[0].author} • {articles[0].date} </Text> <Button href={articles[0].url} className="bg-blue-600 text-white px-6 py-3 rounded font-bold inline-block no-underline" > Read More </Button> </Section> )} <Hr className="border-gray-200 mx-5 my-8" /> {/* Two-Column Articles */} {articles.slice(1, 5).length > 0 && ( <> <Heading as="h2" className="text-2xl font-bold text-gray-900 mx-5 my-4"> More From This Week </Heading> {Array.from({ length: Math.ceil(articles.slice(1, 5).length / 2) }).map((_, rowIndex) => { const leftArticle = articles[1 + rowIndex * 2]; const rightArticle = articles[2 + rowIndex * 2]; return ( <Section key={rowIndex} className="px-5 mb-6"> <Row> {leftArticle && ( <Column className="w-1/2 align-top px-1"> <Img src={leftArticle.image} alt={leftArticle.title} width="280" className="w-full rounded mb-3" /> <Heading as="h3" className="text-lg font-bold text-gray-900 my-3"> {leftArticle.title} </Heading> <Text className="text-sm leading-5 text-gray-500 my-2"> {leftArticle.excerpt} </Text> <Link href={leftArticle.url} className="text-sm text-blue-600 no-underline font-semibold"> Read article → </Link> </Column> )} {rightArticle && ( <Column className="w-1/2 align-top px-1"> <Img src={rightArticle.image} alt={rightArticle.title} width="280" className="w-full rounded mb-3" /> <Heading as="h3" className="text-lg font-bold text-gray-900 my-3"> {rightArticle.title} </Heading> <Text className="text-sm leading-5 text-gray-500 my-2"> {rightArticle.excerpt} </Text> <Link href={rightArticle.url} className="text-sm text-blue-600 no-underline font-semibold"> Read article → </Link> </Column> )} </Row> </Section> ); })} </> )} <Hr className="border-gray-200 mx-5 my-8" /> {/* Footer */} <Section className="bg-gray-50 p-8 mt-8 text-center"> <Text className="text-sm text-gray-500 my-2"> You're receiving this because you subscribed to our newsletter. </Text> <Link href={unsubscribeUrl} className="text-sm text-blue-600 underline block my-2"> Unsubscribe from this list </Link> <Text className="text-sm text-gray-500 my-2"> © 2026 Company Name. All rights reserved. </Text> </Section> </Container> </Body> </Tailwind> </Html> ); } Newsletter.PreviewProps = { articles: [ { title: 'The Future of Web Development in 2026', excerpt: 'Exploring the latest trends and technologies shaping modern web development.', image: 'https://via.placeholder.com/600x300', url: 'https://example.com/article-1', author: 'Jane Doe', date: 'Jan 15, 2026' }, { title: 'React Server Components Explained', excerpt: 'A deep dive into React Server Components and their benefits.', image: 'https://via.placeholder.com/280x140', url: 'https://example.com/article-2', author: 'John Smith', date: 'Jan 14, 2026' }, { title: 'Building Accessible Web Apps', excerpt: 'Best practices for creating inclusive digital experiences.', image: 'https://via.placeholder.com/280x140', url: 'https://example.com/article-3', author: 'Sarah Johnson', date: 'Jan 13, 2026' } ], unsubscribeUrl: 'https://example.com/unsubscribe' } as NewsletterProps;

该示例值得学习的写法

  • "头条 + 双栏"的数据驱动articles[0]恒为头条区;articles.slice(1, 5)取后续文章,通过Math.ceil(length / 2)计算所需行数,再用Array.from生成行,每行内leftArticle = articles[1 + rowIndex * 2]rightArticle = articles[2 + rowIndex * 2]取左右稿。这套索引运算让你传入任意长度的文章数组都能自动排版,且缺失侧(奇数文章)自动留空不报错。
  • Section > Row > Column三层结构还原两栏Columnw-1/2 align-top px-1等宽分栏,左右卡片内联文章信息;这是邮件客户端中唯一可靠的"双列网格"实现方式。
  • Img显式宽高 + 圆角:头条图width="600"className="w-full rounded-lg",双栏图width="280",配合rounded/rounded-lg做圆角裁切;文案中引用页眉 Logo 也显式给出width/height防止布局抖动。
  • 行内Read article →链接:次要阅读入口用<Link>+text-blue-600+箭头弱化呈现,与头条的实心ButtonRead More)形成主次两级 CTA。
  • 合规页脚:底部灰底区块包含说明性文案、"Unsubscribe" 退订链接与版权行。这与 SKILL.md 中"页脚需包含实体地址、退订链接、当前年份"的最佳实践一致(商用模板可扩展为含邮寄地址)。
  • <Head />与内容分节注释:代码内用{/* Header */}{/* Featured Article */}{/* Footer */}注释划分布局段落,提升长模板可维护性。

七、团队邀请邮件:角色信息卡与邀请有效期

最后一个示例聚焦 B2B 协作场景:被邀请人需要一眼看到"谁邀请我、加入哪个团队、我被授予什么角色"。角色信息用独立灰底卡片突出,邀请链接的失效时间也需要明确告知。文档原始实现如下:

import { Html, Head, Preview, Body, Container, Section, Heading, Text, Button, Hr, Tailwind, pixelBasedPreset } from '@react-email/components'; interface TeamInvitationProps { inviterName: string; inviterEmail: string; teamName: string; role: string; inviteUrl: string; expiryDays: number; } export default function TeamInvitation({ inviterName, inviterEmail, teamName, role, inviteUrl, expiryDays }: TeamInvitationProps) { return ( <Html lang="en"> <Tailwind config={{ presets: [pixelBasedPreset] }}> <Head /> <Preview>You've been invited to join {teamName}</Preview> <Body className="bg-gray-100 font-sans"> <Container className="mx-auto py-10 px-5 max-w-xl bg-white"> <Heading className="text-3xl font-bold text-gray-800 text-center mb-6"> You're Invited! </Heading> <Text className="text-base leading-7 text-gray-800 my-4"> <strong>{inviterName}</strong> ({inviterEmail}) has invited you to join the{' '} <strong>{teamName}</strong> team. </Text> <Section className="bg-gray-50 p-5 rounded border border-gray-200 my-6"> <Text className="text-xs text-gray-500 uppercase font-bold mb-2">Role</Text> <Text className="text-lg font-bold text-gray-800 m-0">{role}</Text> </Section> <Text className="text-base leading-7 text-gray-800 my-4"> Click the button below to accept the invitation and get started. </Text> <Button href={inviteUrl} className="bg-green-600 text-white px-7 py-3.5 rounded block text-center font-bold text-base my-6 no-underline" > Accept Invitation </Button> <Hr className="border-gray-200 my-6" /> <Text className="text-sm text-gray-500 leading-5 my-2"> This invitation will expire in {expiryDays} day{expiryDays > 1 ? 's' : ''}. </Text> <Text className="text-sm text-gray-500 leading-5 my-2"> If you weren't expecting this invitation, you can safely ignore this email. </Text> </Container> </Body> </Tailwind> </Html> ); } TeamInvitation.PreviewProps = { inviterName: 'John Doe', inviterEmail: 'john@example.com', teamName: 'Acme Corp Engineering', role: 'Developer', inviteUrl: 'https://example.com/invite/abc123', expiryDays: 7 } as TeamInvitationProps;

该示例值得学习的写法

  • 关键信息用信息卡片突出Role区块以bg-gray-50 p-5 rounded border border-gray-200呈现,内部标签行text-xs uppercase font-bold(全大写灰字微标签)+ 数值行text-lg font-bold。这里的边框同时写了borderborder-gray-200(颜色),符合 STYLING.md 关于"邮件里必须写明边框样式(border-solid 等)、需要时重置其余三边"的提醒;模板作者在浅灰底上用 border + rounded 勾勒卡片边界。
  • 行内强调用<strong>:邀请人、邮箱、团队名内联加粗,读者扫读时先抓这三个实体。
  • CTA 全宽块级 + 品牌绿bg-green-600 ... block text-center no-underline,团队协作场景的确认按钮普遍采用绿色系(积极的语义色)。
  • 有效期信息二段式:先给"过期天数"({expiryDays} day{s}复数处理),再补"非预期邀请可忽略"的安抚话术,兼顾紧迫感与安全提示。
  • <Preview>动态拼接团队名You've been invited to join {teamName},让收件箱摘要直接携带团队上下文,配合较高的邮件打开率。

八、总结:五种模式的可复用要点清单

文档结尾把五个模式验证的共同点浓缩为一份清单,可作为你自行开发新模板时的 checklist:

  • Tailwind CSS 工具类负责全部视觉,无需书写任何外部 CSS 文件;配合Tailwind config可将bg-brand这类自定义色注入(见 SKILL.md 中的theme.extend.colors用法)。
  • 组件使用统一遵循pixelBasedPreset预设,这是邮件端rem单位不可用前提下的标准配置。
  • TypeScript 类型收口 props,每个模板声明独立interface,布尔/可选项显式标注(如expiryHours?: numbersku?: string)。
  • .PreviewProps静态属性固化演示数据,本地 dev server 与测试工具可无数据渲染。
  • 响应式布局全部基于 Section/Row/Column 表格语义实现多栏与对齐,绝不使用 flexbox/grid 与媒体查询。
  • 模板覆盖现实高频场景:密码重置、订单确认、告警通知、Newsletter、团队邀请,可分别作为你所在产品对应邮件模板的起点。

进一步的工程细节——安装方式(create-email脚手架、email dev --dir emails预览命令)、基础模板骨架、组件逐个用法,可继续查阅本仓库的 .agents/skills/react-email/SKILL.md、references/COMPONENTS.md、references/STYLING.md、references/SENDING.md 与 references/I18N.md。如果你正在把 React Email 集成进实际发信链路,也可以对照本仓库其他邮件渲染/模板相关模块的既有实现来对齐工程约束——但请务必以本文所继承的五套「标准答案」作为模板编写的业务骨架。

【免费下载链接】novuThe open-source communication infrastructure for agents and products项目地址: https://gitcode.com/GitHub_Trending/no/novu

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询