vintasend-react-email
v0.13.3
Published
VintaSend template renderer implementation for React Email
Downloads
2,088
Readme
vintasend-react-email
React Email template renderer for VintaSend.
This package provides a BaseEmailTemplateRenderer implementation that can:
- load templates from file paths (
.js,.mjs,.cjs,.ts,.tsx,.mts,.cts) - compile uncompiled TypeScript/TSX templates at runtime
- render React body templates to HTML via
@react-email/render - render templates from inline content (including loops, conditionals, and arbitrary logic)
Installation
npm install vintasend-react-emailExports
ReactEmailTemplateRendererReactEmailTemplateRendererFactoryReactEmailInlineTemplateRendererReactEmailInlineTemplateRendererFactory
Template compilation script
For environments that can only deploy a single source file (e.g. Medplum bots), you can pre-compile template source files into a JSON map and then feed that map to an inline renderer.
This package provides a CLI:
compile-react-email-templates [input-directory] [output-file]Supported template extensions:
.ts,.tsx,.js,.jsx,.mts,.cts,.mjs,.cjs
Add to package.json
{
"scripts": {
"compile-templates": "compile-react-email-templates ./notification-templates ./compiled-react-email-templates.json"
}
}Run
npm run compile-templatesThe output JSON uses template paths (relative to input-directory) as keys and raw template source as values.
How it works
File-based templates (render)
When VintaSend sends a notification, the renderer reads:
notification.bodyTemplatenotification.subjectTemplate
Each file must export a function as either:
- default export:
export default function Template(context) { ... } - named export:
export function render(context) { ... }
The function receives the notification context and returns:
- for subject: usually a string
- for body: a React element or a string
If body returns a React element, it is converted to HTML.
Inline templates (renderFromTemplateContent)
Inline templateContent.subject and templateContent.body are compiled and executed at runtime.
You can pass either:
- Full module source (with
export defaultorexport function render), or - Inline snippet:
- function body (
const x = ...; if (...) ...; return ...;) - or expression (
<div>Hello</div>,'Subject text')
- function body (
This allows logic such as if/else, loops, mapping arrays, etc.
Usage
1) Create renderer instance
import { ReactEmailTemplateRendererFactory } from 'vintasend-react-email';
const templateRenderer = new ReactEmailTemplateRendererFactory<MyConfig>().create();1.1) Create inline renderer instance (for pre-compiled templates)
Use this when your runtime cannot read template files directly and you want to load templates from a JSON map.
import compiledTemplates from './compiled-react-email-templates.json';
import { ReactEmailInlineTemplateRendererFactory } from 'vintasend-react-email';
const inlineTemplateRenderer = new ReactEmailInlineTemplateRendererFactory<MyConfig>().create(
compiledTemplates,
);Expected map shape:
type CompiledTemplates = Record<string, string>;Where keys match the values you pass in notification.bodyTemplate and notification.subjectTemplate.
2) Use with your adapter (example)
// Example with an adapter that accepts a BaseEmailTemplateRenderer implementation
const adapter = new SomeEmailAdapterFactory<MyConfig>().create(templateRenderer);Inline renderer with adapter:
const adapter = new SomeEmailAdapterFactory<MyConfig>().create(inlineTemplateRenderer);3) Template files
subject-template.ts:
export default function SubjectTemplate(context: { name?: string }) {
return `Welcome ${context.name ?? ''}`;
}body-template.tsx:
import React from 'react';
export default function BodyTemplate(context: {
name?: string;
items?: string[];
isVip?: boolean;
}) {
const items = context.items ?? [];
return (
<div>
<h1>Hello {context.name ?? ''}</h1>
{context.isVip ? <p>VIP user</p> : <p>Standard user</p>}
<ul>
{items.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
</div>
);
}Inline content examples
Full module source
const templateContent = {
subject: `
export default function Subject(context) {
return ` + '`' + `Welcome ${context.name ?? ''}` + '`' + `;
}
`,
body: `
import React from 'react';
export default function Body(context) {
return <p>Hello {context.name ?? ''}</p>;
}
`,
};Snippet (function body)
const templateContent = {
subject: `
const tier = context.isVip ? 'VIP' : 'User';
return ` + '`' + `Welcome ${tier} ${context.name ?? ''}` + '`' + `;
`,
body: `
const items = Array.isArray(context.items) ? context.items : [];
return (
<ul>
{items.map((item, index) => (
<li key={index}>{String(item)}</li>
))}
</ul>
);
`,
};Validation behavior
- throws if
subjectTemplate(file-based rendering) is missing - throws if
templateContent.subject(inline rendering) is missing - throws if template file/content does not export a function
Development
npm test
npm run build