HTML Templates
rust-web-server integrates the Tera template engine — a Jinja2-compatible HTML templating system with variables, control flow, filters, and inheritance.
Quick start
- Create a
templates/directory next to your binary. - Call
template::init("templates")once at startup. - Call
template::render("page.html", &ctx)from any handler.
use rust_web_server::template::{self, Context};
fn main() { // Initialize the global template engine once at startup. template::init("templates").expect("failed to load templates");
// Start the server ...}Initialisation
There are two ways to initialise the global engine:
template::init(dir)
Loads all files under dir recursively (equivalent to the glob dir/**/*):
template::init("templates").unwrap();template::init_from_env()
Reads the directory from the RWS_CONFIG_TEMPLATE_DIR environment variable (default: "templates"):
template::init_from_env().unwrap();Both functions return Err if called a second time (the engine is a OnceLock).
Rendering in a handler
use rust_web_server::template::{self, Context};use rust_web_server::request::Request;use rust_web_server::response::Response;use rust_web_server::router::PathParams;use rust_web_server::server::ConnectionInfo;
fn home( _req: &Request, _params: &PathParams, _conn: &ConnectionInfo, _state: &(),) -> Response { let mut ctx = Context::new(); ctx.insert("title", "Welcome"); ctx.insert("items", &["Rust", "rws", "Tera"]); ctx.insert("logged_in", &true);
template::render("index.html", &ctx).unwrap_or_else(|e| { // render() returns Err only when the template file is missing // or contains a syntax error — treat as 500 let mut r = Response::new(); r.status_code = 500; r })}template::render delegates to the global TeraEngine, renders the named template, and returns a 200 OK response with Content-Type: text/html.
The Context type
Context is a re-export of tera::Context. Call .insert(key, value) with any Serialize value:
use rust_web_server::template::Context;use serde::Serialize;
#[derive(Serialize)]struct User { name: String, email: String,}
let user = User { name: "Alice".into(), email: "alice@example.com".into() };
let mut ctx = Context::new();ctx.insert("user", &user);ctx.insert("count", &42_u32);ctx.insert("flags", &["new", "featured"]);Directory structure convention
your-project/├── src/│ └── main.rs└── templates/ ├── base.html <- base layout ├── index.html ├── users/ │ ├── list.html │ └── detail.html └── partials/ └── nav.htmlTemplate names passed to render are relative paths within the templates directory (e.g. "users/list.html").
Tera template syntax
Variables
<h1>{{ title }}</h1><p>Hello, {{ user.name }}!</p><p>Total: {{ count }}</p>Conditionals
{% if logged_in %} <a href="/logout">Logout</a>{% else %} <a href="/login">Login</a>{% endif %}Loops
<ul>{% for item in items %} <li>{{ item }}</li>{% endfor %}</ul>Loop provides a loop variable with helpers: loop.index (1-based), loop.index0 (0-based), loop.first, loop.last.
Template inheritance
Define a base layout:
{# templates/base.html #}<!DOCTYPE html><html><head><title>{% block title %}My Site{% endblock %}</title></head><body> <nav>{% include "partials/nav.html" %}</nav> <main>{% block content %}{% endblock %}</main></body></html>Extend it in a child template:
{# templates/index.html #}{% extends "base.html" %}
{% block title %}Home — My Site{% endblock %}
{% block content %} <h1>{{ heading }}</h1> <p>{{ body }}</p>{% endblock %}Built-in filters
Tera ships many filters you can apply with |:
{{ name | upper }} {# ALICE #}{{ title | truncate(length=20) }}{{ items | length }}{{ price | round(precision=2) }}{{ html_content | safe }} {# disable auto-escaping #}{{ date | date(format="%Y-%m-%d") }}Auto-escaping is enabled by default for {{ }} output — HTML special characters (<, >, &, ") are escaped. Use | safe only for trusted content.
Macros
Define reusable template fragments:
{% macro input(name, label, type="text") %} <label for="{{ name }}">{{ label }}</label> <input type="{{ type }}" id="{{ name }}" name="{{ name }}">{% endmacro %}
{{ self::input(name="email", label="Email", type="email") }}TeraEngine directly
The global singleton wraps TeraEngine. You can also create a standalone engine when you need multiple template directories, or for testing:
use rust_web_server::template::{TeraEngine, Context};
// From a directorylet engine = TeraEngine::from_dir("templates").unwrap();
// From in-memory strings (useful in tests)let engine = TeraEngine::from_raw(&[ ("hello.html", "<p>Hello, {{ name }}!</p>"),]).unwrap();
let mut ctx = Context::new();ctx.insert("name", "World");let html: String = engine.render("hello.html", &ctx).unwrap();let response = engine.response("hello.html", &ctx).unwrap();TeraEngine::from_glob(pattern) accepts any glob pattern Tera accepts, e.g. "templates/**/*.html".
Hot reload
Edit a template file and reload it without restarting the server:
use rust_web_server::template;
template::init("templates").unwrap();// ... edit templates/index.html on disk ...template::reload().unwrap();template::reload():
- Re-globs the whole directory — it picks up edited content in existing files and newly added or removed files matching the original pattern, not just a diff of what changed.
- Is atomic. It builds the full replacement set of templates before swapping it in. If one edited file has a syntax error (or an
{% extends %}/{% import %}chain no longer resolves),reload()returnsErrand the previously-loaded templates keep serving — a bad edit can’t leave the live server with a half-broken template set. - Is wired into
SIGHUPautomatically.config_reload::reload()calls it for you (as a no-op iftemplate::init()was never called), sokill -HUP $(pidof rws)picks up edited templates alongside CORS/rate-limit/TLS-cert changes. Calltemplate::reload()directly only if you want a different trigger — a file-watcher thread, or your own admin endpoint.
The equivalent method exists on a standalone TeraEngine too — engine.reload() — for the multi-directory case described above. It requires the engine to have been built from a glob (from_dir/from_glob/init); an engine built with TeraEngine::from_raw has no directory to re-read and reload() returns Err for it.
use rust_web_server::template::TeraEngine;
let mut engine = TeraEngine::from_dir("templates").unwrap();// ... edit a template file ...engine.reload().unwrap();