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.
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.
- i18n — TemplateContext::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.