Shipping templates from your add-on
One <emailkit:templates> ZCML block. That is the whole registration mechanism — imio.emailkit uses exactly the same directive for its own templates, so it is its own first consumer.
Declare the registration
<!-- imio/pm/notifications/configure.zcml -->
<configure
xmlns="http://namespaces.zope.org/zope"
xmlns:emailkit="http://namespaces.imio.be/emailkit"
i18n_domain="imio.pm.notifications"
>
<include package="imio.emailkit" file="meta.zcml" />
<emailkit:templates directory="templates">
<emailkit:template
name="item_published"
subject="[email_subject_item_published] An item was published"
preheader="[email_preheader_item_published] ..."
/>
<emailkit:template
name="meeting_convocation"
subject="[email_subject_meeting_convocation] Convocation"
/>
</emailkit:templates>
</configure>
- Name
directory- Type
- str, default 'templates'
- Description
Where the compiled
.ptfiles live, relative to the package the ZCML file belongs to.
- Name
subject- Type
- MessageID, '[msgid] Default text'
- Description
Lives in the registration, translated per recipient language at send time. The msgid comes from the bracketed prefix, its domain from the enclosing
i18n_domain; there is no metadata sidecar and no front-matter round-trip.
- Name
preheader- Type
- MessageID, optional
- Description
The hidden inbox-preview line next to the subject — the highest-visibility email feature everyone forgets, and every inbox shows it. Omitted, the div collapses to nothing.
Names are namespaced at lookup: imio.pm.notifications:item_published. The package half of that name is derived from the ZCML file's own package — you never spell it out, so it cannot disagree with reality. An unknown name raises TemplateNotFound carrying the list of names that are registered.
One <emailkit:templates> block per package, and duplicate template
names are a startup error. A second block in the same package, or two
templates sharing a name, raises ConfigurationConflictError when ZCML
loads — not a silent overwrite. overrides.zcml can replace a registration
the same way it replaces any other ZCML-registered component.
i18n caveat. i18ndude extracts msgids from .py and .pt, never from
ZCML. If your add-on rebuilds its .pot with it, restate the same msgids in
a small msgids.py module whose only job is calling the message factory on
each one — exactly what imio.emailkit does for its own templates
(src/imio/emailkit/msgids.py).
Lay the package out
src/imio/pm/notifications/
├── emails/ # Maizzle project — dev-only, excluded from sdist
│ ├── package.json
│ ├── maizzle.config.js # extends the kit base config
│ └── src/templates/
│ ├── item_published.vue
│ └── meeting_convocation.vue
├── templates/ # ✅ committed build output
│ ├── item_published.pt
│ ├── item_published.txt.pt # hand-authored plaintext twin
│ ├── meeting_convocation.pt
│ └── meeting_convocation.txt.pt
└── tests/
├── fixtures/item_published.py
└── golden/item_published.fr.html
In MANIFEST.in:
recursive-include src/imio/pm/notifications/templates *.pt
prune src/imio/pm/notifications/emails
The emails/ sources are dev-only; the compiled .pt files are what ship.
Wire up the buildout
[buildout]
parts = ... emails
[emails]
recipe = imio.recipe.emailkit
eggs = ${instance:eggs}
# compile-on-install = false (default)
# kit-mode = path | copy (default: path)
# node-bin = node
The recipe resolves the part's eggs, collects every package whose ZCML registers <emailkit:templates> (a filesystem marker scan — buildout imports nothing), resolves the design kit from the imio.emailkit egg, and generates three scripts:
- Name
bin/compile-emails [--package NAME] [--watch] [--new NAME]- Description
Wire the kit,
npm ciifnode_modulesis stale against the lockfile, run Maizzle, rename*.htmlto*.pt, move intotemplates/.--new NAMEscaffolds the four files a template needs — a.vueskeleton, a fixture, a golden placeholder and a registration stub to paste.
- Name
bin/check-emails [--package NAME]- Description
The staleness gate plus the authoring lint. This is the CI gate.
- Name
bin/preview-emails [--package NAME]- Description
Watch the
.vuesources, compile, render throughrender()with the committed fixtures, serve with live reload and a language switcher.
The recipe does not compile during buildout by default, and must not. Compiling at buildout time would make Node a production dependency across roughly 350 applications and couple deployments to npm availability. With the default, the recipe imports nothing that knows Node exists and runs no subprocess.
The CI contract
Two gates, for every add-on that ships templates:
bin/check-emails --package <self>— the build output is not stale, and the sources pass the lint.- Golden-file tests — runtime rendering is intact.
What each template needs
<name>.pt— the compiled output, committed.<name>.txt.pt— the hand-authored plaintext twin, where plaintext quality matters. Without it,render()falls back to naive extraction and logs a deprecation.tests/fixtures/<name>.py— aCONTEXTdict.tests/golden/<name>.<lang>.htmland.txt— the snapshots.
bin/compile-emails --new <name> scaffolds all four. Templates have a rigid shape; making the right structure the path of least resistance beats documenting it.
Related
- Authoring rules — read this before writing your first
.vue. - The design kit — the layout and components you compose.
- Testing — fixtures, snapshots and the golden base class.
- Source:
imio/emailkit/discovery.py