Skip to content

Email & Newsletter Templates Architecture

Beta - APIs may change

This module is in beta. The classes, plugins, database schema, and component conventions described in the developer documentation may change in backwards-incompatible ways before the general release. Customizations built against them may need updating between beta versions.

This page explains how Hyva_EmailTemplates (hyva-themes/commerce-module-email-templates) turns a Liveview component tree into the HTML that Magento actually sends. Read this before customizing render behavior or debugging why an email looks different from its preview.

Content Types and Providers

The module registers two Liveview content types on the Hyvä CMS ProviderPool:

Content type Provider Backing entity
email_template Hyva\EmailTemplates\Model\Provider\EmailTemplateProvider Magento\Email\Model\Template
newsletter_template Hyva\EmailTemplates\Model\Provider\NewsletterTemplateProvider Magento\Newsletter\Model\Template

Per-template state (Hyvä editor enabled flag, draft and published component trees, preheader) lives in the module's own tables, hyva_commerce_email_template and hyva_commerce_newsletter_template, each with a cascading foreign key to its Magento parent. The Magento template rows stay untouched, which is why disabling the Hyvä editor cleanly falls back to classic rendering.

Editor routes are registered in etc/adminhtml/di.xml on the EditorRouteRegistry: hyva_cms_email/liveview/editor (email) and hyva_cms_email/liveview/newslettereditor (newsletter).

The Render Pipeline

Both content types share one core service: Model\Render\ComponentTreeRenderer renders the saved component tree to HTML by rendering each component's element template (Hyva\CmsLiveviewEditor\Block\Element + the component's .phtml).

From there, the two types diverge - and the difference matters:

Email: afterGetProcessedTemplate

Plugin\Model\Template plugs into Magento\Email\Model\Template. When a Hyvä-enabled template renders, the plugin builds the full email HTML (component tree → chrome header/footer → root.phtml wrapper), runs it through the Magento email template filter (which resolves {{var}}, {{trans}}, {{layout}}, {{depend}} directives with the real transactional variables), and inlines CSS.

Newsletter: afterGetTemplateText

Plugin\Model\NewsletterTemplate plugs into getTemplateText() instead, and deliberately returns HTML with {{var}} directives still intact. Magento's newsletter queue snapshots getTemplateText() into newsletter_queue.newsletter_text when a campaign is queued; the per-subscriber newsletter filter then resolves each subscriber's personal variables (name, unsubscribe link) at send time. Resolving variables too early would bake one subscriber's data into everyone's newsletter.

Hook placement is load-bearing

If you plug into these models yourself, respect this split: email resolves directives in getProcessedTemplate, newsletter must leave them for the queue. Swapping the hooks breaks personalization.

CSS Inlining and Directive Safety

Model\InlineStyleProcessor inlines <style> rules into element attributes (Gmail strips <style> blocks). Before parsing, it placeholder-protects {{...}} directives so DOMDocument cannot corrupt them - directives survive inlining in both text and attributes.

Preview Pipeline

The editor preview (hyva_cms_email/create/emailpreview, Block\Preview\EmailWrapper) renders the same component tree but resolves directives with dummy data instead of the Magento filter:

  • Plugin\Block\EmailOrderContextInjector injects order_preview_context (customer, items, totals, addresses) into every element block - from a real order when Preview Order (Increment ID) is configured, otherwise from fixtures.
  • Plugin\Block\EmailHtmlVariableSubstitutor + Model\DummyData\VariableSubstitutor resolve {{var}}, {{trans}}, {{depend}}, {{if}} and {{layout}} directives against a dummy variable map (see the class for the full list, including order_data.* and subscriber_data.*).

Element templates can branch on $block->validPreview() when preview needs different markup than production (for example, the Shipment Tracking component renders dummy rows in preview and a {{layout}} directive in production).

Version History

hyva_commerce_email_template_version_history stores versions for both content types, discriminated by an entity_type column (email_template / newsletter_template). The table intentionally has no foreign key (one column cannot reference two parents); cleanup happens at application level via Model\VersionHistoryCleaner, triggered by the delete plugins on both template models.

Newsletter Chrome

Model\Render\NewsletterChrome appends the default newsletter footer (store details + unsubscribe link) and optional logo header, controlled by hyva_cms/newsletter_template/use_footer / use_header. Both the send path and the preview use this one service, so what you preview is what gets sent. The templates are theme-overridable:

  • Hyva_EmailTemplates::newsletter/header/default.phtml
  • Hyva_EmailTemplates::newsletter/footer/default.phtml

Component Scoping

All bundled components declare "context_flags": ["email"] in etc/hyva_cms/components.json, and the email/newsletter editors declare allowed_root_component_context_flags: ['email'] plus strict_component_context: true. The result: email components never appear in CMS page/block pickers, and generic CMS components never appear inside emails (they would render non-email-safe markup). See Creating Email Components for what this means for your own components.