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 .pt files 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.

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 ci if node_modules is stale against the lockfile, run Maizzle, rename *.html to *.pt, move into templates/. --new NAME scaffolds the four files a template needs — a .vue skeleton, 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 .vue sources, compile, render through render() with the committed fixtures, serve with live reload and a language switcher.

The CI contract

Two gates, for every add-on that ships templates:

  1. bin/check-emails --package <self> — the build output is not stale, and the sources pass the lint.
  2. 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 — a CONTEXT dict.
  • tests/golden/<name>.<lang>.html and .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.