Template Engines

The Template capability renders HTML (or any text) from a named template and a data context. It fronts the major server-side template languages — Tera, Handlebars, Liquid — behind one trait so pages, emails, and webhook payloads share the same render machinery regardless of which engine a project prefers.

When To Use It

- Server-rendered pages that do not need React / hydration.

- Transactional email bodies.

- Rendered webhook payloads (Slack blocks, Discord embeds, SMS).

  • Any text format where you want variable substitution and a familiar tag syntax.

If you want React SSR with auto-islands, use PRISM instead. Reach for Template when the output is classic server-rendered HTML or a non-HTML text document.

Tip

For user-authored templates in a multi-tenant app, prefer @bext/tmpl-liquid — Liquid's sandboxed execution model prevents arbitrary code paths that Tera and Handlebars permit.

The Trait

pub trait TemplatePlugin: Send + Sync {
    fn name(&self) -> &str;
    fn language(&self) -> &str;  // "tera", "handlebars", "liquid"
    fn register(&self, name: &str, source: &str) -> Result<(), TemplateError>;
    fn render(
        &self,
        template: &str,
        ctx: &TemplateContext,
    ) -> Result<String, TemplateError>;
}

Each plugin keeps its own registry keyed by template name. register compiles and stores; render looks up and evaluates. The split lets the host cache compile work across requests without guessing the backend's lifecycle.

Key Types

Type Purpose
TemplateContext data: HashMap<String, TemplateValue> plus optional locale (BCP-47 tag).
TemplateValue ABI-flat value tree (Null, Bool, Int, Float, String, Array, Object) with From<serde_json::Value> bridges both ways.
TemplateError TemplateNotFound (404), SyntaxError { template, message } (500), RenderError (500), Backend (500).

TemplateValue mirrors serde_json::Value but lives in bext-plugin-api so WASM guests can depend on it without pulling serde_json transitively.

Locale Handling

TemplateContext::locale is an optional BCP-47 tag ("en-US", "fr-FR"). When present, the host injects it into every template as the __locale variable so i18n filters and partials can branch on it without threading it through their own args:

<html lang="{{ __locale }}">

Plugins that do not consume the locale silently ignore it.

Reference Implementations

Plugin Language Strengths
@bext/tmpl-tera Tera (Jinja2-like) Rust-native, rich filters, inheritance, macros.
@bext/tmpl-handlebars Handlebars Logic-less mustache extended with helpers. Easy for front-end engineers.
@bext/tmpl-liquid Liquid (Shopify) Safe for user-provided templates — no arbitrary code execution paths.

All three plugins implement the same trait; switching backends is a config change, not a code change.

Picking A Backend

Scenario Pick
Maximum expressiveness, Rust-first project @bext/tmpl-tera.
Logic-less templates, front-end engineers write them @bext/tmpl-handlebars.
User-authored templates in a multi-tenant app @bext/tmpl-liquid.

Example

use bext_plugin_api::template::*;

// Register at startup (usually driven by file scan)
let plugin: &dyn TemplatePlugin = /* @bext/tmpl-tera */;
plugin.register(
    "welcome",
    r#"
    <h1>Hi {{ name }}!</h1>
    <p>You've signed up for {{ plan }}. Locale: {{ __locale }}.</p>
    "#,
)?;

// Render per request
let ctx = TemplateContext::new()
    .set("name", "Alice")
    .set("plan", "Pro")
    .with_locale("en-US");

let html = plugin.render("welcome", &ctx)?;

Output:

<h1>Hi Alice!</h1>
<p>You've signed up for Pro. Locale: en-US.</p>

Feature Flag

None. The Template trait and types live in bext-plugin-api and are always available; no cargo feature gates them.

See Also

- @bext/tmpl-tera — Rust-first Jinja-like engine.

- @bext/tmpl-handlebars — logic-less mustache superset.

- @bext/tmpl-liquid — sandbox-friendly engine for user templates.

- Mailer — send transactional email; pair with a template plugin to render the body.

- i18nTemplateContext::locale bridges directly into bext's i18n layer.

- PRISM framework — the alternative for React SSR with auto-islands and client hydration.

- Capabilities overview — the full list of pluggable capabilities.