Improvement: Email template tokens

Hi @daftspunk,

I have a idea for improving the email templates.

Create a new tab in the secondary tabs when editing a template, titled “Tokens,” where the content would include instructions in Markdown format explaining which tokens are available and how to use them.

My approach was:
I added a custom mail template token provider concept for my plugin SmartForms.

OctoberCMS already supports registered mail templates via registerMailTemplates() and Twig variables passed through Mail::send(), but there is no native way to describe which variables are available for a specific mail template in the backend editor.

My solution introduces a token provider class responsible for three things:

  1. Declaring available tokens

The provider returns grouped token definitions, for example:

{{ formName }}
{{ referenceKey }}
{{ placeholders.name }}
{% for name, detail in fieldDetails %} ... {% endfor %}
  1. Building runtime variables

The same provider prepares the actual data passed to Mail::send(), so the token list shown in the backend and the real email variables come from the same source.

  1. Providing documentation

The provider can load Markdown instructions, which are displayed in a custom Tokens tab inside the mail template editor.

The plugin keeps mail template codes defined once, using constants and registerMailTemplates(). The token provider only checks whether a template is supported based on those registered codes.

Where each provider could expose:

  getTokenGroups()
  getInstructionsMarkdown()
  makeVariables(array $context)

I think this feature would be very helpful for creating emails.

What you think?

1 Like

Hi @mcore, great to hear from you again! This is a genuinely useful idea. Discoverability of available variables is one of the rougher edges of the mail template editor right now, especially for end users customizing a plugin’s emails.

Taking a step back though, with the ethos of “as simple as possible, but no simpler,” I think the token provider concept might be one layer of abstraction too many. The calling code already knows what it passes to Mail::send(), so a provider that also “builds runtime variables” is duplicating that knowledge.

What if the API is purely documentation? Something like registerMailTemplateVariables(), returning a map of template code patterns to variable docs. The backend reads it, renders a “Variables” tab, and that’s it. Mail sending stays untouched. If the docs drift from reality, that’s the same problem as any docblock and lives with the plugin author.

There’s a nice side effect to this approach too: by asking developers to declare their template variables up front, it nudges them to be more mindful about what they’re exposing and to keep naming consistent across templates. Right now it’s easy to pass ad-hoc $data arrays to Mail::send() and end up with customer in one template, user in another, buyer in a third. A declared convention encourages plugin authors to settle on a vocabulary and stick to it.

Scoped declarations with wildcard support. Register variables against a template code pattern, e.g. shop:* for every template in the shop namespace, shop:order_* for a sub-group, or shop:order_shipped for a single template. Common variables (customer, store) are declared once at the namespace level, and template-specific ones (tracking_number) layer on top.

Object properties. Since most of these variables are objects, the docs need to describe their useful properties too:

public function registerMailTemplateVariables()
{
    return [
        'shop:*' => [
            'customer' => [
                'description' => 'The currently logged in customer',
                'properties' => [
                    'name' => 'Full name',
                    'email' => 'Email address',
                    'is_guest' => 'True if checking out as guest',
                ],
            ],
            'store' => [
                'description' => 'The store configuration',
                'properties' => [
                    'name' => 'Store display name',
                    'url' => 'Storefront URL',
                ],
            ],
        ],
        'shop:order_*' => [
            'order' => [
                'description' => 'The order being processed',
                'properties' => [
                    'reference' => 'Human-readable order reference',
                    'total' => 'Formatted order total',
                    'items' => 'Collection of OrderItem (iterable)',
                ],
            ],
        ],
    ];
}

With a shorthand 'variableName' => 'description' when there are no properties to document. I’d lean against adding formal type metadata ('type' => 'collection' etc.) and just let authors describe shape in prose. One level of nesting feels like the right ceiling; anything deeper and we’re rebuilding a type system.

Your idea of a markdown instructions blob is still useful as an optional escape hatch for explaining loops, conditionals, or anything the structured form can’t capture cleanly. Could live alongside the variable map under the same registration method.

What do you think? Does this cover the cases you had in mind for SmartForms?

2 Likes

Yeah, I haven’t been here in a while.

That’s a great idea; it’ll definitely cover what’s needed. :star_struck:

1 Like