A collection of ready-to-use email templates using MJML and a reusable component system.
- Features
- Structure
- Installation
- Quick Start
- Available Components
- Internationalization
- Customizing Theme
- Creating a New Template
- Examples
- Utility Functions
- Theme System - Centralized configuration for colors, fonts, and styles
- Reusable Components - Ready-to-use sections for building emails
- Responsive Design - All templates are responsive thanks to MJML
- TypeScript Support - Full TypeScript support with type definitions
- Internationalization - Built-in support for multiple languages with per-template translations
- Easy Extension - Simple way to add new templates
- Unified API - Single
renderTemplatefunction for all templates
npm install @codee_team/medusa-notification-templatesThe package includes mjml as a dependency, so you don't need to install it separately.
After installation, import templates where you send notifications:
import { renderTemplate } from "@codee_team/medusa-notification-templates";If you prefer to copy the templates folder directly:
- Copy the
templatesfolder to your notification module - Install dependencies:
npm install mjml npm install --save-dev @types/mjml
- Import templates:
import { renderTemplate } from "./templates/emails";
import { renderTemplate, ContactFormTemplateData } from "@codee_team/medusa-notification-templates";
const templateName = "contact-form";
const data: ContactFormTemplateData = {
subject: "New contact form message",
name: "John Doe",
email: "john@example.com",
phone: "+1 234 567 890",
message: "Hello, I would like to..."
};
const { html, text } = renderTemplate(templateName, data, { locale: "pl" });
// Use with Medusa notification service
await notificationService.createNotifications({
to: "admin@example.com",
channel: "email",
template: templateName,
data: {
html,
text,
subject: data.subject,
},
});All components are available from shared/components:
import {
headerSection,
footerSection,
textSection,
dividerSection,
buttonSection,
richTextSection,
Theme,
} from "@codee_team/medusa-notification-templates";Header section with colored background.
headerSection("Email Title", { theme })Footer section with message.
footerSection("Footer message", { theme })Section with label and value (e.g., "Email: john@example.com").
textSection("Email", "john@example.com", { theme })Horizontal separator.
dividerSection({ theme })Call-to-action button.
buttonSection("View Order", "https://example.com/order/123", {
theme,
align: "center" // "left" | "center" | "right"
})Section with formatted HTML text.
richTextSection("This is <strong>important</strong> message", {
theme,
align: "center"
})Templates support multiple languages through the i18n system. Each template has its own translations in the locales/ folder. The default language is Polish (pl), but you can use English (en) or add more languages.
import { renderTemplate } from "@codee_team/medusa-notification-templates";
// Polish (default)
const { html: htmlPL } = renderTemplate("contact-form", data);
// English
const { html: htmlEN } = renderTemplate("contact-form", data, { locale: "en" });- Create a new locale file in the template's
locales/folder (e.g.,de.tsfor German) - Add translations following the structure in
locales/types.ts - Export it in
locales/index.ts:
// locales/de.ts
import { ContactFormTranslations } from "./types";
export const de: ContactFormTranslations = {
labels: {
name: "Name",
email: "E-Mail",
phone: "Telefon",
message: "Nachricht",
},
footer: "Diese Nachricht wurde automatisch vom Kontaktformular gesendet",
};// locales/index.ts
import { ContactFormTranslations } from "./types";
import { Locale } from "../../../shared/i18n";
import { pl } from "./pl";
import { en } from "./en";
import { de } from "./de";
export const translations: Record<Locale, ContactFormTranslations> = {
pl,
en,
de, // Add new language
};- Update
Localetype inshared/i18n/types.ts:
export type Locale = "pl" | "en" | "de";- Use it:
renderTemplate("contact-form", data, { locale: "de" })
You can customize colors, fonts, and other styles by modifying shared/theme/presets/default/index.ts or by passing a custom theme to template functions:
import { Theme } from "@codee_team/medusa-notification-templates";
const customTheme: Theme = {
colors: {
primary: "#FF5733",
primaryText: "#ffffff",
background: "#ffffff",
surface: "#f5f5f5",
text: {
primary: "#000000",
secondary: "#333333",
muted: "#999999",
},
border: "#e0e8f0",
},
fonts: {
primary: "Roboto",
fallback: "sans-serif",
},
spacing: {
section: "20px 20px 20px 20px",
text: "0",
divider: "0 30px",
},
typography: {
header: {
fontSize: "20px",
fontWeight: "600",
lineHeight: "24px",
},
label: {
fontSize: "11px",
fontWeight: "600",
textTransform: "uppercase",
letterSpacing: "1px",
},
body: {
fontSize: "16px",
lineHeight: "24px",
},
footer: {
fontSize: "12px",
},
},
};
const { html } = renderTemplate("contact-form", data, { theme: customTheme });mkdir templates/emails/my-new-template
mkdir templates/emails/my-new-template/localesexport type MyNewTemplateData = {
subject: string;
userName: string;
userEmail: string;
customField?: string;
};Create translation files for each language:
// locales/types.ts
export interface MyNewTemplateTranslations {
greeting: string;
labels: {
userName: string;
userEmail: string;
};
footer: string;
}// locales/pl.ts
import { MyNewTemplateTranslations } from "./types";
export const pl: MyNewTemplateTranslations = {
greeting: "Witaj",
labels: {
userName: "Nazwa użytkownika",
userEmail: "Email",
},
footer: "Wiadomość automatyczna",
};// locales/en.ts
import { MyNewTemplateTranslations } from "./types";
export const en: MyNewTemplateTranslations = {
greeting: "Hello",
labels: {
userName: "User Name",
userEmail: "Email",
},
footer: "Automated message",
};// locales/index.ts
import { MyNewTemplateTranslations } from "./types";
import { Locale } from "../../../shared/i18n";
import { pl } from "./pl";
import { en } from "./en";
export const translations: Record<Locale, MyNewTemplateTranslations> = {
pl,
en,
};
export { type MyNewTemplateTranslations } from "./types";
export { type Locale } from "../../../shared/i18n";
export { pl, en };import {
headerSection,
footerSection,
textSection,
dividerSection,
escapeHtml,
Theme,
} from "../../shared/components";
import { getTranslations } from "../../shared/i18n";
import { translations, Locale } from "./locales";
import { MyNewTemplateData } from "./types";
interface MyNewTemplateMainOptions {
theme?: Theme;
locale?: Locale;
}
export function getMyNewTemplateMain(
data: MyNewTemplateData,
options: MyNewTemplateMainOptions = {}
): string {
const theme = options.theme;
const locale = options.locale || "pl";
const t = getTranslations(translations, locale);
return `
${headerSection(data.subject, { theme })}
${textSection(t.labels.userName, escapeHtml(data.userName), { theme })}
${dividerSection({ theme })}
${textSection(t.labels.userEmail, escapeHtml(data.userEmail), { theme })}
${footerSection(t.footer, { theme })}
`.trim();
}import mjml2html from "mjml";
import { escapeHtml, Theme } from "../../shared/components";
import { getTranslations } from "../../shared/i18n";
import { translations, Locale } from "./locales";
import { getMyNewTemplateMain } from "./main";
import { MyNewTemplateData } from "./types";
interface MyNewTemplateOptions {
theme?: Theme;
locale?: Locale;
}
export function getMyNewTemplateHtml(
data: MyNewTemplateData,
options: MyNewTemplateOptions = {}
): string {
return mjml2html(
`
<mjml>
<mj-head>
<mj-title>${escapeHtml(data.subject)}</mj-title>
</mj-head>
<mj-body>
${getMyNewTemplateMain(data, options)}
</mj-body>
</mjml>
`,
{
keepComments: false,
}
).html;
}
export function getMyNewTemplateText(
data: MyNewTemplateData,
options: MyNewTemplateOptions = {}
): string {
const locale = options.locale || "pl";
const t = getTranslations(translations, locale);
return `
${data.subject}
${t.labels.userName}: ${data.userName}
${t.labels.userEmail}: ${data.userEmail}
---
${t.footer}
`.trim();
}Add your template to the registry:
import { getMyNewTemplateHtml, getMyNewTemplateText } from "./my-new-template";
import { MyNewTemplateData } from "./my-new-template/types";
export type TemplateName = "contact-form" | "order-created" | "my-new-template";
export type TemplateData = ContactFormTemplateData | OrderCreatedTemplateData | MyNewTemplateData;
const templateRegistry: Record<TemplateName, TemplateRenderer> = {
"contact-form": {
getHtml: getContactFormHtml,
getText: getContactFormText,
},
"order-created": {
getHtml: getOrderCreatedHtml,
getText: getOrderCreatedText,
},
"my-new-template": {
getHtml: getMyNewTemplateHtml,
getText: getMyNewTemplateText,
},
};
// Add type-safe overload
export function renderTemplate(
templateName: "my-new-template",
data: MyNewTemplateData,
options?: TemplateOptions
): { html: string; text: string };import { renderTemplate } from "@codee_team/medusa-notification-templates";
const { html, text } = renderTemplate("my-new-template", data, { locale: "pl" });See implementation in emails/contact-form/ - a simple form with text fields.
See implementation in emails/order-created/ - a more complex template with product list, CTA button, and shipping address.
Escapes HTML in text to prevent XSS attacks. Available from shared/utils:
import { escapeHtml } from "@codee_team/medusa-notification-templates";
const safeText = escapeHtml("<script>alert('xss')</script>");
// Returns: "<script>alert('xss')</script>"If you have questions or suggestions, please create an issue in the GitHub repository.