email-template-developer
Use this skill when working on email, SMS, or push notification templates — authoring Handlebars HTML email templates, understanding the backend template engine, visual testing with Playwright, or managing multi-tenant template deployment. Triggers on: body.hbs.html, subject.hbs.txt, Handlebars e...
SKILL.md
Full skill instructions
Email Template Developer
Author and maintain notification templates across the Handlebars template ecosystem. Covers template authoring conventions, HTML email constraints, visual testing, backend template processing, and multi-tenant deployment.
Scope: Template authoring and backend template engine understanding. For general backend service work, use the
backend-developerskill.
Repositories
<!-- TODO: Replace with your actual repository locations -->| Repo | Location | Purpose |
|---|---|---|
| Notification-Templates | <!-- TODO: Add location --> | Template authoring, Handlebars HTML, visual testing |
| Notification-Service | <!-- TODO: Add location --> | Backend notification service, template engine, tenant config |
Template Directory Structure
Notification-Templates/
├── .github/
│ ├── instructions/
│ │ └── copilot.instructions.md # HTML email development guidelines
│ └── prompts/
│ ├── create_new_template.prompt.md
│ ├── create_screenshots.prompt.md
│ └── validate_urls.prompt.md
├── Templates/
│ ├── EMAIL/
│ │ ├── _MASTER_TEMPLATE/ # Base for ALL new templates
│ │ │ ├── DE/body.hbs.html + subject.hbs.txt
│ │ │ ├── EN/body.hbs.html + subject.hbs.txt
│ │ │ ├── FR/body.hbs.html + subject.hbs.txt
│ │ │ └── IT/body.hbs.html + subject.hbs.txt
│ │ └── <TEMPLATE_NAME>/
│ │ ├── DE/body.hbs.html + subject.hbs.txt
│ │ ├── EN/body.hbs.html + subject.hbs.txt
│ │ ├── FR/body.hbs.html + subject.hbs.txt
│ │ └── IT/body.hbs.html + subject.hbs.txt
│ ├── SMS/
│ │ └── <TEMPLATE_NAME>/
│ │ └── <LANG>/body.hbs.txt
│ ├── PUSH/
│ │ └── <TEMPLATE_NAME>/
│ │ └── <LANG>/body.hbs.txt + title.hbs.txt
│ ├── DEFINITION/
│ │ ├── _MASTER_TEMPLATE.json
│ │ └── <TEMPLATE_NAME>.json
│ └── global_testdata.json # Shared test data for all templates
├── package.json # Node.js: handlebars, playwright, pixelmatch
└── playwright.config.js # Chromium, 800x1200 viewport, threshold 0.2
Golden Rules
- ALWAYS use
_MASTER_TEMPLATEas base for new email templates — never start from scratch. - 4 languages always: DE, EN, FR, IT — every template must have all 4 language variants.
- No translations in
body.hbs.html— the HTML body structure is identical across languages. Onlysubject.hbs.txtdiffers per language. - Preserve block comment anchors — comments like
<!-- Content 1-spaltig -->,<!-- Address block -->serve as structural markers and must be kept. - Stay under ~100kB per template to avoid Gmail truncation.
Existing Copilot Prompts
The repo includes pre-built prompts in /.github/prompts/ — use them instead of reinventing workflows:
| Prompt | Purpose |
|---|---|
/create_new_template | Copy _MASTER_TEMPLATE → new template, set subjects per language |
/create_screenshots | Replace placeholders with global_testdata.json, generate Playwright screenshots to /.temp/ |
/validate_urls | Check all template URLs per language, output CSV to /.temp/URL_Validation_Results.csv |
Creating a New Template (Workflow)
- Copy
/Templates/EMAIL/_MASTER_TEMPLATE/→/Templates/EMAIL/<NEW_NAME>/ - Copy
/Templates/DEFINITION/_MASTER_TEMPLATE.json→/Templates/DEFINITION/<NEW_NAME>.json - Update subjects in each
subject.hbs.txtper language - Update
<title>tags in eachbody.hbs.html - Replace
_MASTER_TEMPLATEreferences with the new template name - Modify HTML body content as needed (same structure across all languages)
- Generate screenshots and validate
HTML Email Constraints
Cross-Client Compatibility
Target clients (last 24 months): Apple Mail, Outlook (classic + new), Gmail, Yahoo Mail.
| Constraint | Rule |
|---|---|
| Layout | Table-based responsive, max-width: 620px |
| Background | background-color: #f4f4f4 outer wrapper |
| Buttons | Bulletproof (VML fallback for Outlook), min 44×44px tap target |
| Contrast | WCAG AA minimum |
| Format detection | x-ms-format-detection="none" to suppress auto-linking |
| Outlook conditionals | <!--[if mso]> and <!--[if !mso]><!--> ... <!--<![endif]--> |
| Dark mode | color-scheme: light dark, prefers-color-scheme media query, neutral base palette |
| Validation | Audit against Can I Email before finalizing |
Template HTML Structure
<!-- Typical block structure (simplified) -->
<table role="presentation" width="100%" style="background-color: #f4f4f4;">
<tr><td align="center">
<table width="620" style="max-width: 620px;">
<!-- Logo -->
<!-- Content 1-spaltig -->
<!-- Content 2-spaltig (optional) -->
<!-- Address block -->
<!-- Disclaimer -->
</table>
</td></tr>
</table>
Size Optimization
Keep templates compact. Refactor verbose markup into block-based structure. Remove redundant inline styles by consolidating shared styles. Minimize whitespace in production templates.
Brand Colors
<!-- TODO: Replace with your organization's brand colors -->| Name | RGB | Hex |
|---|---|---|
| primary-red | rgb(216, 32, 52) | #D82034 |
| anthrazit | rgb(53, 53, 53) | #353535 |
| white | rgb(255, 255, 255) | #FFFFFF |
| light-grey | rgb(244, 244, 244) | #F4F4F4 |
| warm-grey | rgb(189, 183, 175) | #BDB7AF |
| dark-warm-grey | rgb(148, 142, 136) | #948E88 |
| sand | rgb(233, 227, 218) | #E9E3DA |
| burgundy | rgb(147, 25, 66) | #931942 |
| midnight-blue | rgb(31, 51, 84) | #1F3354 |
| forest-green | rgb(81, 117, 95) | #51755F |
| ocean-blue | rgb(72, 122, 150) | #487A96 |
| petrol | rgb(47, 107, 109) | #2F6B6D |
| clay | rgb(190, 135, 100) | #BE8764 |
| mauve | rgb(162, 128, 148) | #A28094 |
| golden-yellow | rgb(209, 168, 70) | #D1A846 |
Handlebars Syntax
Placeholders
{{ Salutation }}
{{ FirstName }}
{{ LastName }}
Conditionals
{{#if DisplayDebugInfo}}
<p>Debug: {{Environment}}-{{NotificationId}}</p>
{{#if RobotSessionId}}Robot: {{RobotSessionId}}{{/if}}
{{/if}}
Iteration
{{#each Docs}}
<tr><td>{{Name}}</td><td>{{Date}}</td></tr>
{{/each}}
Test Data
All placeholders map to keys in Templates/global_testdata.json. This file contains ~100+ fields including:
Salutation(per-language variants: "Sehr geehrter Herr", "Dear Mr", "Cher Monsieur", "Gentile Signor")FirstName,LastName, personal data fieldsDocsarray,EV_Contracts,ThreeAStartdataDisplayDebugInfo,Environment,NotificationId,RobotSessionId
When creating screenshots, resolve placeholders with this test data. Handle language-specific overrides (e.g., Salutation changes per language).
Visual Testing
Playwright Screenshots
npm run screenshots # Generate screenshots for all templates
npm test # Run Playwright visual regression tests
npm run test:update # Update baseline snapshots
npm run test:ui # Interactive Playwright UI
npm run test:report # View HTML report
- Viewport: 800×1200, Chromium only
- Comparison:
pixelmatchwith threshold0.2, binary mode - Output: Screenshots saved to
/.temp/<TEMPLATE_NAME>_<LANG>.png - Process: Replace Handlebars placeholders with
global_testdata.jsondata → render in Playwright → capture full-page screenshot
Section Testing
Use testi.at for testing individual sections or new components before integrating them into a full template.
Backend Template Engine
File Name Constants (WellKnown.cs)
EMAIL: body.hbs.html + subject.hbs.txt
SMS: body.hbs.txt
PUSH: body.hbs.txt + title.hbs.txt
Folder names: EMAIL, SMS, PUSH, DEFINITION
Two Template Engines
| Engine | Type | When Used |
|---|---|---|
| Handlebars | File-based (Blob Storage) | Default for all channels and content types |
| Render | API-based | Email channel only, when application is specified and tenant has Render configured |
Selection logic (TemplateEngineSelector):
- Render: Email + non-null application + tenant has Render config → uses Render
- Handlebars: everything else (fallback for all channels)
- No cross-engine fallback — if the selected engine fails, the request fails
Handlebars Processing Chain
Request → TemplateEngineSelector
→ TenantAwareTemplateResolver (resolve tenant config)
→ HandlebarsTemplateRepository (fetch from Azure Blob Storage)
→ HandlebarsCompiledTemplatesCache (memory cache: 24h absolute / 1h sliding)
→ HandlebarsContentParser.ParseAsync(tenantId, templateKey, data)
→ HandlebarsDotNet compilation + rendering
Blob Storage Path
Templates stored at: {channel}/{TEMPLATE_NAME}/{languageCode}/{fileName}
Example: EMAIL/ADDRESS_CHANGE_NOTIFICATION/DE/body.hbs.html
Backend Models
// Core template types
record EmailTemplate(TemplateCompositeKey Id, string SubjectTemplate, string BodyTemplate);
record TemplateCompositeKey(string TemplateName, string LanguageCode);
record TemplateKey(string TemplateName, string LanguageCode, ChannelType ChannelType, ContentFieldType ContentFieldType);
// Enums
enum ChannelType { Email, Sms, Push }
enum ContentFieldType { Body, Subject }
Multi-Tenant Architecture
Tenants
<!-- TODO: Replace with your actual tenant configuration -->| Tenant | ID | Default Lang | Supported Languages |
|---|---|---|---|
| TenantA | tenanta | de | de, fr, it, en |
| TenantB | tenantb | de | de, fr, it, en |
| TenantC | tenantc | de | de, fr, it, en |
Each tenant has:
- Isolated blob storage for templates (own container)
- Own provider configuration (SendGrid, Twilio, template engines)
- Feature flags controlling which channels are enabled
Template Provider Configuration
Templates = new TemplateProvidersConfiguration
{
RenderDebugInfo = "Tenants:{TenantId}:ProvidersConfiguration:Templates:RenderDebugInfo",
Handlebars = new HandlebarsTemplateProviderConfiguration
{
BlobStorage = new BlobStorageConfiguration
{
ConnectionString = "Tenants:{TenantId}:...:BlobStorage:ConnectionString",
Url = "Tenants:{TenantId}:...:BlobStorage:Url",
ContainerName = "templates"
}
},
Render = new RenderTemplateProviderConfiguration // Optional, Email-only
{
TenantName = "chid",
Applications = [] // Empty = allow all
}
}
Template Deployment
- Create templates in the Notification-Templates repo
- Test with Playwright screenshots and
global_testdata.json - Upload to tenant-specific Azure Blob Storage
- Verify by sending test notifications in the tenant context
Deployment methods:
- Azure Storage Explorer (manual)
- CI/CD pipeline with Azure CLI
- Notification CLI tool (
notification-cli publish)
API Tenancy Requirements
All Notification API requests require:
- Header:
X-Tenant-Id: {tenant-id} - OAuth scopes:
api.notification.access+notification.tenant.{tenant-id}
SMS & Push Templates
SMS Template Pattern
Your verification code is: {{Code}}
{{#if DisplayDebugInfo}}
{{Environment}}-{{NotificationId}}
{{#if RobotSessionId}}Robot:{{RobotSessionId}}{{/if}}
{{/if}}
Push Template Pattern
title.hbs.txt— notification titlebody.hbs.txt— notification body
Both use same Handlebars syntax as email templates, without HTML.
Common Pitfalls
- Starting from scratch instead of
_MASTER_TEMPLATE— always copy the master. - Translating body HTML — the body structure is language-independent; only subjects differ.
- Missing a language — every template needs DE, EN, FR, IT.
- Ignoring Outlook — always test MSO conditional comments and VML button fallbacks.
- Gmail truncation — keep total HTML under ~100kB.
- Forgetting debug info block — include
{{#if DisplayDebugInfo}}section for diagnostics. - Wrong file names — must be exactly
body.hbs.htmlandsubject.hbs.txt(backendWellKnown.csconstants). - Missing test data keys — verify all placeholders exist in
global_testdata.jsonbefore generating screenshots.
