项目概述
本项目实现一个面向 email rendering 的完整系统,核心要解决的问题是 rendering templates with variable substitution and inline CSS so emails look right across clients。整体由 Template loader、Variable binder、CSS inliner、Subject renderer、Preview generator 五个模块组成,每个模块单一职责、可独立测试,最后通过集成测试验证端到端正确性。设计上把 an unescaped variable that injects markup and breaks the email layout in a strict client 作为第一类需求先行处理,而非事后再补。
架构与关键决策
数据模型为 a template with HTML body and subject, rendered with a variable context and post-processed to inline styles for client compatibility。Template loader 定义其余阶段共同依赖的数据形状并在边界处校验输入;Variable binder 承载 email rendering 的正确性逻辑;CSS inliner 专门处理 an unescaped variable that injects markup and breaks the email layout in a strict client,在失败路径上给出明确的拒绝、重试或降级策略;Subject renderer 提供可观测性,每次操作输出带关联 id 的结构化事件;Preview generator 在过载时按既定方式降级,避免过载演变为数据损坏。配置与代码分离,使风险变更可经 diff 审阅。
实施与测试
按五个阶段依次实现 Template loader、Variable binder、CSS inliner、Subject renderer、Preview generator,每阶段配套聚焦单测。集成测试组合各模块并驱动真实 email rendering 场景:先验证正常路径,再刻意触发 an unescaped variable that injects markup and breaks the email layout in a strict client 并断言系统降级而非损坏。所有依赖均在进程内或被桩替换,用例在 CI 中确定可复现。
限制与后续
- 单区域设计,多区域复制超出范围。
- Template loader 默认进程内,未水平共享。
- 无鉴权层,假定部署在强制身份的边缘之后。
代码示例
# setup for email-renderer-service
pnpm init
pnpm add handlebars juice fastify
pnpm add -D typescript tsx vitest
email_renderer_service/
├── src/index.ts
├── src/template-loader.ts
├── src/variable-binder.ts
├── tests/email_renderer_service.test.ts
└── package.json
// Template loader: email rendering core
export function templateloader(input: TemplateloaderInput): TemplateloaderResult {
// Build the template loader that anchors email rendering and defines the core data shape.
return processTemplateloader(input);
}
// Variable binder — handles email rendering Add the variable binder so
export class Variablebinder {
constructor(private opts: VariablebinderOptions) {}
async run(ctx: Context): Promise<VariablebinderOutput> {
return this.execute(ctx);
}
}
// CSS inliner: email rendering stage
export const cssinliner = (next: Next) => async (req: Request) => {
const out = await transform(req);
return next(out);
};
// Subject renderer for email rendering
export async function subjectrenderer(stream: AsyncIterable<Chunk>): Promise<Aggregate> {
let acc = init();
for await (const c of stream) acc = merge(acc, c);
return acc;
}
// Preview generator: email rendering finalization
export function previewgenerator(state: State): Result {
validate(state);
return finalize(state);
}
# run the email rendering test suite
pnpm vitest run