Skip to content
email-template-developer logo

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-developer skill.

Repositories

<!-- TODO: Replace with your actual repository locations -->
RepoLocationPurpose
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

  1. ALWAYS use _MASTER_TEMPLATE as base for new email templates — never start from scratch.
  2. 4 languages always: DE, EN, FR, IT — every template must have all 4 language variants.
  3. No translations in body.hbs.html — the HTML body structure is identical across languages. Only subject.hbs.txt differs per language.
  4. Preserve block comment anchors — comments like <!-- Content 1-spaltig -->, <!-- Address block --> serve as structural markers and must be kept.
  5. 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:

PromptPurpose
/​create_new_templateCopy _MASTER_TEMPLATE → new template, set subjects per language
/​create_screenshotsReplace placeholders with global_testdata.json, generate Playwright screenshots to /​.temp/
/​validate_urlsCheck all template URLs per language, output CSV to /​.temp/​URL_Validation_Results.csv

Creating a New Template (Workflow)

  1. Copy /​Templates/​EMAIL/​_MASTER_TEMPLATE/ → /​Templates/​EMAIL/<NEW_NAME>/
  2. Copy /​Templates/​DEFINITION/​_MASTER_TEMPLATE.json → /​Templates/​DEFINITION/<NEW_NAME>.json
  3. Update subjects in each subject.hbs.txt per language
  4. Update <title> tags in each body.hbs.html
  5. Replace _MASTER_TEMPLATE references with the new template name
  6. Modify HTML body content as needed (same structure across all languages)
  7. Generate screenshots and validate

HTML Email Constraints

Cross-Client Compatibility

Target clients (last 24 months): Apple Mail, Outlook (classic + new), Gmail, Yahoo Mail.

ConstraintRule
LayoutTable-based responsive, max-width: 620px
Backgroundbackground-color: #f4f4f4 outer wrapper
ButtonsBulletproof (VML fallback for Outlook), min 44×44px tap target
ContrastWCAG AA minimum
Format detectionx-ms-format-detection="none" to suppress auto-linking
Outlook conditionals<!--[if mso]> and <!--[if !mso]><!--> ... <!--<![endif]-->
Dark modecolor-scheme: light dark, prefers-color-scheme media query, neutral base palette
ValidationAudit 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 -->
NameRGBHex
primary-redrgb(216, 32, 52)#D82034
anthrazitrgb(53, 53, 53)#353535
whitergb(255, 255, 255)#FFFFFF
light-greyrgb(244, 244, 244)#F4F4F4
warm-greyrgb(189, 183, 175)#BDB7AF
dark-warm-greyrgb(148, 142, 136)#948E88
sandrgb(233, 227, 218)#E9E3DA
burgundyrgb(147, 25, 66)#931942
midnight-bluergb(31, 51, 84)#1F3354
forest-greenrgb(81, 117, 95)#51755F
ocean-bluergb(72, 122, 150)#487A96
petrolrgb(47, 107, 109)#2F6B6D
clayrgb(190, 135, 100)#BE8764
mauvergb(162, 128, 148)#A28094
golden-yellowrgb(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 fields
  • Docs array, EV_Contracts, ThreeAStart data
  • DisplayDebugInfo, 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: pixelmatch with threshold 0.2, binary mode
  • Output: Screenshots saved to /​.temp/<TEMPLATE_NAME>_<LANG>.png
  • Process: Replace Handlebars placeholders with global_testdata.json data → 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

EngineTypeWhen Used
HandlebarsFile-based (Blob Storage)Default for all channels and content types
RenderAPI-basedEmail 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 -->
TenantIDDefault LangSupported Languages
TenantAtenantadede, fr, it, en
TenantBtenantbdede, fr, it, en
TenantCtenantcdede, 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

  1. Create templates in the Notification-Templates repo
  2. Test with Playwright screenshots and global_testdata.json
  3. Upload to tenant-specific Azure Blob Storage
  4. 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 title
  • body.hbs.txt — notification body

Both use same Handlebars syntax as email templates, without HTML.

Common Pitfalls

  1. Starting from scratch instead of _MASTER_TEMPLATE — always copy the master.
  2. Translating body HTML — the body structure is language-independent; only subjects differ.
  3. Missing a language — every template needs DE, EN, FR, IT.
  4. Ignoring Outlook — always test MSO conditional comments and VML button fallbacks.
  5. Gmail truncation — keep total HTML under ~100kB.
  6. Forgetting debug info block — include {{#if DisplayDebugInfo}} section for diagnostics.
  7. Wrong file names — must be exactly body.hbs.html and subject.hbs.txt (backend WellKnown.cs constants).
  8. Missing test data keys — verify all placeholders exist in global_testdata.json before generating screenshots.