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
| Repo |
Location |
Purpose |
| Notification-Templates |
|
Template authoring, Handlebars HTML, visual testing |
| Notification-Service |
|
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_TEMPLATE as 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. Only subject.hbs.txt differs 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.txt per language
- Update
<title> tags in each body.hbs.html
- Replace
_MASTER_TEMPLATE references 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
| 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 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
| 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
| 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 title
body.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.html and subject.hbs.txt (backend WellKnown.cs constants).
- Missing test data keys — verify all placeholders exist in
global_testdata.json before generating screenshots.
1---2name: email-template-developer3description: 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 email templates, notification templates, email branding, _MASTER_TEMPLATE, template screenshots, Playwright visual regression, notification tenancy, template blob storage, EMAIL/SMS/PUSH channels, or cross-client HTML email compatibility.4---5
6# Email Template Developer
7
8Author 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.
9
10> **Scope**: Template authoring and backend template engine understanding. For general backend service work, use the `backend-developer` skill.
11
12## Repositories
13
14<!-- TODO: Replace with your actual repository locations -->
15| Repo | Location | Purpose |
16|---|---|---|
17| Notification-Templates | <!-- TODO: Add location --> | Template authoring, Handlebars HTML, visual testing |
18| Notification-Service | <!-- TODO: Add location --> | Backend notification service, template engine, tenant config |
19
20## Template Directory Structure
21
22```
23Notification-Templates/
24├── .github/
25│ ├── instructions/
26│ │ └── copilot.instructions.md # HTML email development guidelines
27│ └── prompts/
28│ ├── create_new_template.prompt.md
29│ ├── create_screenshots.prompt.md
30│ └── validate_urls.prompt.md
31├── Templates/
32│ ├── EMAIL/
33│ │ ├── _MASTER_TEMPLATE/ # Base for ALL new templates
34│ │ │ ├── DE/body.hbs.html + subject.hbs.txt
35│ │ │ ├── EN/body.hbs.html + subject.hbs.txt
36│ │ │ ├── FR/body.hbs.html + subject.hbs.txt
37│ │ │ └── IT/body.hbs.html + subject.hbs.txt
38│ │ └── <TEMPLATE_NAME>/
39│ │ ├── DE/body.hbs.html + subject.hbs.txt
40│ │ ├── EN/body.hbs.html + subject.hbs.txt
41│ │ ├── FR/body.hbs.html + subject.hbs.txt
42│ │ └── IT/body.hbs.html + subject.hbs.txt
43│ ├── SMS/
44│ │ └── <TEMPLATE_NAME>/
45│ │ └── <LANG>/body.hbs.txt
46│ ├── PUSH/
47│ │ └── <TEMPLATE_NAME>/
48│ │ └── <LANG>/body.hbs.txt + title.hbs.txt
49│ ├── DEFINITION/
50│ │ ├── _MASTER_TEMPLATE.json
51│ │ └── <TEMPLATE_NAME>.json
52│ └── global_testdata.json # Shared test data for all templates
53├── package.json # Node.js: handlebars, playwright, pixelmatch
54└── playwright.config.js # Chromium, 800x1200 viewport, threshold 0.2
55```
56
57## Golden Rules
58
591. **ALWAYS use `_MASTER_TEMPLATE` as base** for new email templates — never start from scratch.
602. **4 languages always**: DE, EN, FR, IT — every template must have all 4 language variants.
613. **No translations in `body.hbs.html`** — the HTML body structure is identical across languages. Only `subject.hbs.txt` differs per language.
624. **Preserve block comment anchors** — comments like `<!-- Content 1-spaltig -->`, `<!-- Address block -->` serve as structural markers and must be kept.
635. **Stay under ~100kB** per template to avoid Gmail truncation.
64
65## Existing Copilot Prompts
66
67The repo includes pre-built prompts in `/.github/prompts/` — use them instead of reinventing workflows:
68
69| Prompt | Purpose |
70|---|---|
71| `/create_new_template` | Copy `_MASTER_TEMPLATE` → new template, set subjects per language |
72| `/create_screenshots` | Replace placeholders with `global_testdata.json`, generate Playwright screenshots to `/.temp/` |
73| `/validate_urls` | Check all template URLs per language, output CSV to `/.temp/URL_Validation_Results.csv` |
74
75### Creating a New Template (Workflow)
76
771. Copy `/Templates/EMAIL/_MASTER_TEMPLATE/` → `/Templates/EMAIL/<NEW_NAME>/`
782. Copy `/Templates/DEFINITION/_MASTER_TEMPLATE.json` → `/Templates/DEFINITION/<NEW_NAME>.json`
793. Update subjects in each `subject.hbs.txt` per language
804. Update `<title>` tags in each `body.hbs.html`
815. Replace `_MASTER_TEMPLATE` references with the new template name
826. Modify HTML body content as needed (same structure across all languages)
837. Generate screenshots and validate
84
85## HTML Email Constraints
86
87### Cross-Client Compatibility
88
89Target clients (last 24 months): **Apple Mail, Outlook (classic + new), Gmail, Yahoo Mail**.
90
91| Constraint | Rule |
92|---|---|
93| Layout | Table-based responsive, `max-width: 620px` |
94| Background | `background-color: #f4f4f4` outer wrapper |
95| Buttons | Bulletproof (VML fallback for Outlook), min `44×44px` tap target |
96| Contrast | WCAG AA minimum |
97| Format detection | `x-ms-format-detection="none"` to suppress auto-linking |
98| Outlook conditionals | `<!--[if mso]>` and `<!--[if !mso]><!--> ... <!--<![endif]-->` |
99| Dark mode | `color-scheme: light dark`, `prefers-color-scheme` media query, neutral base palette |
100| Validation | Audit against [Can I Email](https://www.caniemail.com/) before finalizing |
101
102### Template HTML Structure
103
104```html
105<!-- Typical block structure (simplified) -->
106<table role="presentation" width="100%" style="background-color: #f4f4f4;">
107 <tr><td align="center">
108 <table width="620" style="max-width: 620px;">
109 <!-- Logo -->
110 <!-- Content 1-spaltig -->
111 <!-- Content 2-spaltig (optional) -->
112 <!-- Address block -->
113 <!-- Disclaimer -->
114 </table>
115 </td></tr>
116</table>
117```
118
119### Size Optimization
120
121Keep templates compact. Refactor verbose markup into block-based structure. Remove redundant inline styles by consolidating shared styles. Minimize whitespace in production templates.
122
123## Brand Colors
124
125<!-- TODO: Replace with your organization's brand colors -->
126| Name | RGB | Hex |
127|---|---|---|
128| primary-red | `rgb(216, 32, 52)` | `#D82034` |
129| anthrazit | `rgb(53, 53, 53)` | `#353535` |
130| white | `rgb(255, 255, 255)` | `#FFFFFF` |
131| light-grey | `rgb(244, 244, 244)` | `#F4F4F4` |
132| warm-grey | `rgb(189, 183, 175)` | `#BDB7AF` |
133| dark-warm-grey | `rgb(148, 142, 136)` | `#948E88` |
134| sand | `rgb(233, 227, 218)` | `#E9E3DA` |
135| burgundy | `rgb(147, 25, 66)` | `#931942` |
136| midnight-blue | `rgb(31, 51, 84)` | `#1F3354` |
137| forest-green | `rgb(81, 117, 95)` | `#51755F` |
138| ocean-blue | `rgb(72, 122, 150)` | `#487A96` |
139| petrol | `rgb(47, 107, 109)` | `#2F6B6D` |
140| clay | `rgb(190, 135, 100)` | `#BE8764` |
141| mauve | `rgb(162, 128, 148)` | `#A28094` |
142| golden-yellow | `rgb(209, 168, 70)` | `#D1A846` |
143
144## Handlebars Syntax
145
146### Placeholders
147
148```handlebars
149{{ Salutation }}
150{{ FirstName }}
151{{ LastName }}
152```
153
154### Conditionals
155
156```handlebars
157{{#if DisplayDebugInfo}}
158 <p>Debug: {{Environment}}-{{NotificationId}}</p>
159 {{#if RobotSessionId}}Robot: {{RobotSessionId}}{{/if}}
160{{/if}}
161```
162
163### Iteration
164
165```handlebars
166{{#each Docs}}
167 <tr><td>{{Name}}</td><td>{{Date}}</td></tr>
168{{/each}}
169```
170
171### Test Data
172
173All placeholders map to keys in `Templates/global_testdata.json`. This file contains ~100+ fields including:
174- `Salutation` (per-language variants: "Sehr geehrter Herr", "Dear Mr", "Cher Monsieur", "Gentile Signor")
175- `FirstName`, `LastName`, personal data fields
176- `Docs` array, `EV_Contracts`, `ThreeAStart` data
177- `DisplayDebugInfo`, `Environment`, `NotificationId`, `RobotSessionId`
178
179When creating screenshots, resolve placeholders with this test data. Handle language-specific overrides (e.g., Salutation changes per language).
180
181## Visual Testing
182
183### Playwright Screenshots
184
185```bash
186npm run screenshots # Generate screenshots for all templates
187npm test # Run Playwright visual regression tests
188npm run test:update # Update baseline snapshots
189npm run test:ui # Interactive Playwright UI
190npm run test:report # View HTML report
191```
192
193- **Viewport**: 800×1200, Chromium only
194- **Comparison**: `pixelmatch` with threshold `0.2`, binary mode
195- **Output**: Screenshots saved to `/.temp/<TEMPLATE_NAME>_<LANG>.png`
196- **Process**: Replace Handlebars placeholders with `global_testdata.json` data → render in Playwright → capture full-page screenshot
197
198### Section Testing
199
200Use [testi.at](https://testi.at/) for testing individual sections or new components before integrating them into a full template.
201
202## Backend Template Engine
203
204### File Name Constants (WellKnown.cs)
205
206```
207EMAIL: body.hbs.html + subject.hbs.txt
208SMS: body.hbs.txt
209PUSH: body.hbs.txt + title.hbs.txt
210```
211
212Folder names: `EMAIL`, `SMS`, `PUSH`, `DEFINITION`
213
214### Two Template Engines
215
216| Engine | Type | When Used |
217|---|---|---|
218| **Handlebars** | File-based (Blob Storage) | Default for all channels and content types |
219| **Render** | API-based | Email channel only, when `application` is specified and tenant has Render configured |
220
221**Selection logic** (`TemplateEngineSelector`):
222- Render: Email + non-null application + tenant has Render config → uses Render
223- Handlebars: everything else (fallback for all channels)
224- No cross-engine fallback — if the selected engine fails, the request fails
225
226### Handlebars Processing Chain
227
228```
229Request → TemplateEngineSelector
230 → TenantAwareTemplateResolver (resolve tenant config)
231 → HandlebarsTemplateRepository (fetch from Azure Blob Storage)
232 → HandlebarsCompiledTemplatesCache (memory cache: 24h absolute / 1h sliding)
233 → HandlebarsContentParser.ParseAsync(tenantId, templateKey, data)
234 → HandlebarsDotNet compilation + rendering
235```
236
237### Blob Storage Path
238
239Templates stored at: `{channel}/{TEMPLATE_NAME}/{languageCode}/{fileName}`
240
241Example: `EMAIL/ADDRESS_CHANGE_NOTIFICATION/DE/body.hbs.html`
242
243### Backend Models
244
245```csharp
246// Core template types
247record EmailTemplate(TemplateCompositeKey Id, string SubjectTemplate, string BodyTemplate);
248record TemplateCompositeKey(string TemplateName, string LanguageCode);
249record TemplateKey(string TemplateName, string LanguageCode, ChannelType ChannelType, ContentFieldType ContentFieldType);
250
251// Enums
252enum ChannelType { Email, Sms, Push }
253enum ContentFieldType { Body, Subject }
254```
255
256## Multi-Tenant Architecture
257
258### Tenants
259
260<!-- TODO: Replace with your actual tenant configuration -->
261| Tenant | ID | Default Lang | Supported Languages |
262|---|---|---|---|
263| TenantA | `tenanta` | de | de, fr, it, en |
264| TenantB | `tenantb` | de | de, fr, it, en |
265| TenantC | `tenantc` | de | de, fr, it, en |
266
267Each tenant has:
268- **Isolated blob storage** for templates (own container)
269- **Own provider configuration** (SendGrid, Twilio, template engines)
270- **Feature flags** controlling which channels are enabled
271
272### Template Provider Configuration
273
274```csharp
275Templates = new TemplateProvidersConfiguration
276{
277 RenderDebugInfo = "Tenants:{TenantId}:ProvidersConfiguration:Templates:RenderDebugInfo",
278 Handlebars = new HandlebarsTemplateProviderConfiguration
279 {
280 BlobStorage = new BlobStorageConfiguration
281 {
282 ConnectionString = "Tenants:{TenantId}:...:BlobStorage:ConnectionString",
283 Url = "Tenants:{TenantId}:...:BlobStorage:Url",
284 ContainerName = "templates"
285 }
286 },
287 Render = new RenderTemplateProviderConfiguration // Optional, Email-only
288 {
289 TenantName = "chid",
290 Applications = [] // Empty = allow all
291 }
292}
293```
294
295### Template Deployment
296
2971. **Create** templates in the Notification-Templates repo
2982. **Test** with Playwright screenshots and `global_testdata.json`
2993. **Upload** to tenant-specific Azure Blob Storage
3004. **Verify** by sending test notifications in the tenant context
301
302Deployment methods:
303- Azure Storage Explorer (manual)
304- CI/CD pipeline with Azure CLI
305- Notification CLI tool (`notification-cli publish`)
306
307### API Tenancy Requirements
308
309All Notification API requests require:
310- **Header**: `X-Tenant-Id: {tenant-id}`
311- **OAuth scopes**: `api.notification.access` + `notification.tenant.{tenant-id}`
312
313## SMS & Push Templates
314
315### SMS Template Pattern
316
317```handlebars
318Your verification code is: {{Code}}
319{{#if DisplayDebugInfo}}
320{{Environment}}-{{NotificationId}}
321{{#if RobotSessionId}}Robot:{{RobotSessionId}}{{/if}}
322{{/if}}
323```
324
325### Push Template Pattern
326
327- `title.hbs.txt` — notification title
328- `body.hbs.txt` — notification body
329
330Both use same Handlebars syntax as email templates, without HTML.
331
332## Common Pitfalls
333
3341. **Starting from scratch** instead of `_MASTER_TEMPLATE` — always copy the master.
3352. **Translating body HTML** — the body structure is language-independent; only subjects differ.
3363. **Missing a language** — every template needs DE, EN, FR, IT.
3374. **Ignoring Outlook** — always test MSO conditional comments and VML button fallbacks.
3385. **Gmail truncation** — keep total HTML under ~100kB.
3396. **Forgetting debug info block** — include `{{#if DisplayDebugInfo}}` section for diagnostics.
3407. **Wrong file names** — must be exactly `body.hbs.html` and `subject.hbs.txt` (backend `WellKnown.cs` constants).
3418. **Missing test data keys** — verify all placeholders exist in `global_testdata.json` before generating screenshots.