This guide explains how to create new templates for the plugin using the block-based system. Templates can be created for both Email and Slack channels.
Templates are defined using a block-based system where each template consists of:
- Blocks Array: Array of blocks defining the template structure
- Translations: Translation files (JSON format) for internationalization
- Template Registration: Registration in the template service
The block system automatically handles:
- Variable interpolation (
{{data.*}}and{{translations.*}}) - Translation merging
- Channel-specific rendering
Create a new folder in the appropriate channel directory:
For Email templates: src/templates/emails/your-template-name/
For Slack templates: src/templates/slack/your-template-name/
Structure:
your-template-name/
├── config.ts # Template configuration (blocks + translations)
├── translations/ # Translation files
│ ├── pl.json
│ ├── en.json
│ └── index.ts
Start by creating translation files in JSON format:
translations/pl.json:
{
"general": {
"headerTitle": "Tytuł z {{data.variable}}",
"headerDescription": "Opis emaila",
"labels": {
"field1": "Etykieta pola 1",
"field2": "Etykieta pola 2"
},
"footer": "Stopka emaila"
}
}translations/en.json:
{
"general": {
"headerTitle": "Title with {{data.variable}}",
"headerDescription": "Email description",
"labels": {
"field1": "Field 1 label",
"field2": "Field 2 label"
},
"footer": "Email footer"
}
}translations/index.ts:
import pl from "./pl.json";
import en from "./en.json";
export { pl, en };
export const translations: Record<any, any> = {
pl: pl,
en: en,
};config.ts - This file exports both blocks and translations:
import pl from "./translations/pl.json";
import en from "./translations/en.json";
export { pl, en };
export const translations: Record<any, any> = {
pl: pl,
en: en,
};
/**
* Your template blocks
*
* Blocks are automatically interpolated with data and translations
*/
export const templateBlocks = [
{
type: "section",
id: "section-1",
props: {
blocks: [
{
type: "heading",
id: "heading-1",
props: {
value: "{{translations.headerTitle}}"
}
},
{
type: "text",
id: "text-1",
props: {
value: "{{translations.headerDescription}}"
}
}
]
}
},
{
type: "separator",
id: "separator-1"
},
{
type: "section",
id: "section-2",
props: {
blocks: [
{
type: "row",
id: "row-1",
props: {
label: "{{translations.labels.field1}}",
value: "{{data.field1}}"
}
},
{
type: "separator",
id: "separator-2"
},
{
type: "row",
id: "row-2",
props: {
label: "{{translations.labels.field2}}",
value: "{{data.field2}}"
}
}
]
}
},
{
type: "separator",
id: "separator-3"
},
{
type: "section",
id: "section-3",
props: {
blocks: [
{
type: "text",
id: "text-2",
props: {
value: "{{translations.footer}}"
}
}
]
}
}
];import pl from "./translations/pl.json";
import en from "./translations/en.json";
export { pl, en };
export const translations: Record<any, any> = {
pl: pl,
en: en,
};
/**
* Your template blocks
*
* Uses Slack Block Kit format
* All text and url properties are automatically interpolated
*/
export const templateBlocks = [
{
type: "header",
text: {
type: "plain_text",
text: "{{translations.headerTitle}}"
}
},
{
type: "section",
text: {
type: "mrkdwn",
text: "{{translations.headerDescription}}"
}
},
{
type: "section",
fields: [
{
type: "mrkdwn",
text: "*{{translations.labels.field1}}*\n{{data.field1}}"
},
{
type: "mrkdwn",
text: "*{{translations.labels.field2}}*\n{{data.field2}}"
}
]
},
{
type: "actions",
elements: [
{
type: "button",
text: {
type: "plain_text",
text: "{{translations.actions.viewDetails}}"
},
url: "https://example.com/{{data.id}}",
style: "primary"
}
]
}
];Add your template to the appropriate service file:
src/templates/emails/email-template-service.ts:
import {
templateBlocks as YourTemplateBlocks,
translations as yourTemplateTranslations,
} from "./your-template-name"
// In initializeDefaultTemplates():
this.registerTemplate(TEMPLATES_NAMES.YOUR_TEMPLATE, {
...this.baseTemplateConfig,
getConfig: (): any => {
return {
blocks: YourTemplateBlocks,
translations: yourTemplateTranslations,
}
},
})src/templates/slack/slack-template-service.ts:
import {
templateBlocks as YourTemplateBlocks,
translations as yourTemplateTranslations,
} from "./your-template-name"
// In initializeDefaultTemplates():
this.registerTemplate(TEMPLATES_NAMES.YOUR_TEMPLATE, {
...this.baseTemplateConfig,
getConfig: (): any => {
return {
blocks: YourTemplateBlocks,
translations: yourTemplateTranslations,
}
},
})Add your template name to the constants:
For Email: src/templates/emails/types.ts
For Slack: src/templates/slack/types.ts
export const TEMPLATES_NAMES = {
// ... existing templates
YOUR_TEMPLATE: "your-template-name",
} as const;Once registered, you can use your template:
import { emailService, TEMPLATES_NAMES } from "@codee-sh/medusa-plugin-notification-emails/templates/emails"
const { html, text, subject } = await emailService.render({
templateName: TEMPLATES_NAMES.YOUR_TEMPLATE,
data: {
variable: "value",
field1: "Value 1",
field2: "Value 2"
},
options: { locale: "pl" }
})import { slackService, TEMPLATES_NAMES } from "@codee-sh/medusa-plugin-notification-emails/templates/slack"
const { blocks } = await slackService.render({
templateName: TEMPLATES_NAMES.YOUR_TEMPLATE,
data: {
variable: "value",
field1: "Value 1",
field2: "Value 2",
id: "123"
},
options: { locale: "en" }
})For dynamic lists in email templates, use the repeater block:
{
type: "repeater",
id: "repeater-1",
props: {
arrayPath: "items",
itemBlocks: [
{
type: "product-item",
props: {
label: "{{translations.labels.product}}",
thumbnail: "{{data.items.thumbnail}}",
value: "{{data.items.title}} - {{data.items.quantity}}x"
}
}
]
}
}Note: Inside itemBlocks, {{data.*}} variables are resolved relative to the current array item. The arrayPath is automatically prepended.
For dynamic fields in Slack sections, use fieldsPath and fieldTemplate:
{
type: "section",
fieldsPath: "inventory_level.stock_locations",
fieldTemplate: {
type: "plain_text",
text: "{{data.inventory_level.stock_locations.name}}"
}
}The system automatically creates a fields array from the array at fieldsPath.
Access data using {{data.*}} syntax:
{
type: "text",
props: {
value: "Order {{data.order.id}}"
}
}Access translations using {{translations.*}} syntax:
{
type: "heading",
props: {
value: "{{translations.headerTitle}}"
}
}Both support nested property access:
{
type: "row",
props: {
label: "{{translations.labels.orderTotal}}",
value: "{{data.order.transformed.summary.total}}"
}
}Translations can also contain data variables:
{
"general": {
"headerTitle": "Order #{{data.order.id}}"
}
}The system automatically interpolates {{data.*}} variables inside translation values.
- Use meaningful IDs: Always provide unique
idvalues for blocks - Group with sections: Use
sectionblocks to group related content - Use translations: Always use
{{translations.*}}for user-facing text - Test both locales: Ensure translations work for all supported locales
- Validate data structure: Document expected data structure for your template
- Follow existing patterns: Look at existing templates for reference
- Use appropriate blocks: Choose the right block type for your content
- Test interpolation: Verify that all variables are correctly interpolated
- Keep blocks simple: Don't over-nest blocks unnecessarily
- Document your template: Add comments explaining the template structure
section- Container for grouping blocksheading- Heading texttext- Plain text contentrow- Label-value pairproduct-item- Product display with thumbnailrepeater- Repeat blocks for array itemsseparator- Horizontal separator line
See Blocks Documentation for detailed block specifications.
header- Header blocksection- Section block with text or fieldsactions- Action buttons- All other Slack Block Kit block types
See Blocks Documentation and Slack Block Kit Documentation for details.
You can also register templates at runtime without modifying the service files:
import { emailService } from "@codee-sh/medusa-plugin-notification-emails/templates/emails"
emailService.registerTemplate("runtime-template", {
...emailService.getBaseTemplate(),
getConfig: () => ({
blocks: [
{
type: "section",
props: {
blocks: [
{
type: "heading",
props: {
value: "{{translations.title}}"
}
}
]
}
}
],
translations: {
pl: {
general: {
title: "Tytuł"
}
},
en: {
general: {
title: "Title"
}
}
}
})
})- Blocks Documentation - Understanding the block system
- Templates Documentation - Using templates
- Translations Documentation - Translation system details
- Slack Block Kit Documentation - Official Slack Block Kit reference
- React Email Documentation - React Email components