Template Engine
Oxidite provides a powerful template engine for server-side rendering. The engine supports Jinja2-style syntax with features like variable interpolation, control structures, and template inheritance.
Basic Template Usage
Setting Up the Template Engine
First, you need to set up the template engine:
use oxidite::prelude::*;
use oxidite_template::{TemplateEngine, Context};
async fn setup_template_example(_req: Request) -> Result<Response> {
// Create a new template engine
let templates = TemplateContext::new("templates");
// Add a simple template
engine.add_template(
"hello",
"<h1>Hello {{ name }}!</h1><p>Welcome to {{ framework }}.</p>"
)?;
// Create context with data
let mut context = Context::new();
context.set("name", "Developer");
context.set("framework", "Oxidite");
// Render the template as an HTML response
let response = engine.render_response("hello", &context)?;
Ok(response)
}
Loading Templates from Files
You can load templates from a directory structure:
use std::path::PathBuf;
async fn file_templates_example(_req: Request) -> Result<Response> {
let templates = TemplateContext::new("templates");
// Load all templates from a directory (assuming you have template files)
let templates_dir = PathBuf::from("templates");
let count = engine.load_dir(&templates_dir)?;
println!("Loaded {} templates", count);
let mut context = Context::new();
context.set("title", "My Page");
context.set("content", "Page content here");
let response = engine.render_response("index.html", &context)?;
Ok(response)
}
Template Syntax
Variables
Variables in templates are wrapped in {{ }}:
<p>Hello {{ name }}!</p>
<p>Your email is {{ user.email }}.</p> <!-- Dotted notation -->
Control Structures
The template engine supports basic control structures:
<!-- Conditionals -->
{% if user.admin %}
<p>Welcome, administrator!</p>
{% else %}
<p>Welcome, {{ user.name }}!</p>
{% endif %}
<!-- Loops -->
<ul>
{% for item in items %}
<li>{{ item }}</li>
{% endfor %}
</ul>
Template Context
The Context struct is used to pass data to templates:
use oxidite_template::Context;
use serde_json::json;
// Create context in different ways
let mut context = Context::new();
// Set simple values
context.set("name", "Alice");
context.set("age", 30);
// Set complex objects
context.set("user", json!({
"name": "Bob",
"email": "bob@example.com",
"active": true
}));
// Set arrays
context.set("items", vec!["apple", "banana", "cherry"]);
// Create context from JSON
let json_data = json!({
"title": "My Blog",
"posts": [
{"title": "Post 1", "content": "Content 1"},
{"title": "Post 2", "content": "Content 2"}
]
});
let context = Context::from_json(json_data);
Rendering Templates
You can render templates in several ways:
Render to String
use oxidite_template::{TemplateEngine, Context};
let templates = TemplateContext::new("templates");
engine.add_template("greeting", "Hello {{ name }}!")?;
let mut context = Context::new();
context.set("name", "World");
let html = engine.render("greeting", &context)?;
assert_eq!(html, "Hello World!");
Render Directly as Response
use oxidite::prelude::*;
use oxidite_template::{TemplateEngine, Context};
async fn render_as_response(_req: Request) -> Result<Response> {
let templates = TemplateContext::new("templates");
engine.add_template("page", "<h1>{{ title }}</h1><div>{{ content }}</div>")?;
let mut context = Context::new();
context.set("title", "My Page");
context.set("content", "Page content");
// Render directly as HTML response
let response = engine.render_response("page", &context)?;
Ok(response)
}
Template Inheritance
Template inheritance allows you to create base templates that other templates can extend:
Base template (base.html):
<!DOCTYPE html>
<html>
<head>
<title>{% block title %}Default Title{% endblock %}</title>
</head>
<body>
<header>
{% block header %}
<h1>Default Header</h1>
{% endblock %}
</header>
<main>
{% block content %}{% endblock %}
</main>
<footer>
{% block footer %}
<p>© 2025</p>
{% endblock %}
</footer>
</body>
</html>
Child template (page.html):
{% extends "base.html" %}
{% block title %}My Page Title{% endblock %}
{% block content %}
<h2>Page Content</h2>
<p>This is the main content of the page.</p>
{% endblock %}
Filters
Filters allow you to transform variables:
<!-- Uppercase filter -->
<p>{{ name | upper }}</p>
<!-- Length filter -->
<p>Items count: {{ items | length }}</p>
<!-- Default value if variable is not set -->
<p>Name: {{ user.name | default("Anonymous") }}</p>
The safe Filter (Disable HTML Escaping)
By default, the template engine escapes all variable output to prevent XSS attacks. If you need to render raw HTML content (e.g., markdown-to-HTML output, CMS content), use the safe or raw filter:
<!-- Raw HTML output (use with trusted content only!) -->
{{ html_content | safe }}
{{ markdown_output | raw }}
Warning: Only use
safe/rawwith trusted content. Never use it with user-provided input, as it disables XSS protection.
Conditionals and Comparisons
Comparison Operators
The template engine supports comparison operators in {% if %} conditions:
<!-- Equality -->
{% if status == "active" %}
<span class="badge active">Active</span>
{% endif %}
<!-- Inequality -->
{% if role != "guest" %}
<a href="/admin">Admin Panel</a>
{% endif %}
<!-- Numeric comparisons -->
{% if count > 0 %}
<p>You have {{ count }} items</p>
{% endif %}
{% if score >= 90 %}
<span class="grade">A</span>
{% endif %}
elif (Else If) Chains
Use {% elif %} for multi-branch conditions without deep nesting:
{% if status == "draft" %}
<span class="badge draft">Draft</span>
{% elif status == "published" %}
<span class="badge published">Published</span>
{% elif status == "archived" %}
<span class="badge archived">Archived</span>
{% else %}
<span class="badge unknown">Unknown</span>
{% endif %}
Logical Operators
Combine conditions with and, or, and not:
{% if user.admin and user.active %}
<a href="/admin">Admin</a>
{% endif %}
{% if is_premium or is_trial %}
<p>Thank you for using premium features!</p>
{% endif %}
{% if not logged_in %}
<a href="/login">Log in</a>
{% endif %}
Template Includes
Static Includes
Include another template using a string literal:
{% include "partials/header.html" %}
<main>{% block content %}{% endblock %}</main>
{% include "partials/footer.html" %}
Dynamic Includes
You can also include templates using a variable, enabling dynamic template composition:
{% include current_chapter %}
With context:
let mut context = Context::new();
context.set("current_chapter", "chapters/01.html");
This is useful for rendering different partials based on route parameters or application state.
Static File Serving
The template engine also includes utilities for serving static files:
use oxidite::prelude::*;
use oxidite_template::serve_static;
// In your router, register the static file handler
// Note: This should be registered last to avoid blocking other routes
// router.get("/*", serve_static); // Serves files from "public" directory
Complete Example
Here’s a complete example showing template usage in a web application:
use oxidite::prelude::*;
use oxidite_template::{TemplateEngine, Context};
use serde_json::json;
struct AppState {
template_engine: TemplateEngine,
}
async fn home_page(state: State<AppState>) -> Result<Response> {
let mut context = Context::new();
context.set("title", "Home Page");
context.set("welcome_message", "Welcome to our application!");
context.set("features", vec![
"Fast performance",
"Easy to use",
"Type-safe",
"Full-featured"
]);
let response = state.template_engine
.render_response("home.html", &context)?;
Ok(response)
}
async fn blog_page(state: State<AppState>) -> Result<Response> {
let posts = vec![
json!({"title": "First Post", "date": "2025-01-01", "excerpt": "This is the first post"}),
json!({"title": "Second Post", "date": "2025-01-02", "excerpt": "This is the second post"}),
];
let mut context = Context::new();
context.set("title", "Blog");
context.set("posts", posts);
let response = state.template_engine
.render_response("blog.html", &context)?;
Ok(response)
}
#[tokio::main]
async fn main() -> Result<()> {
// Set up template engine
let mut template_engine = TemplateEngine::new();
// Add some templates
template_engine.add_template("home", r#"
<!DOCTYPE html>
<html>
<head><title>{{ title }}</title></head>
<body>
<h1>{{ welcome_message }}</h1>
<ul>
{% for feature in features %}
<li>{{ feature }}</li>
{% endfor %}
</ul>
</body>
</html>
"#)?;
template_engine.add_template("blog", r#"
<!DOCTYPE html>
<html>
<head><title>{{ title }}</title></head>
<body>
<h1>{{ title }}</h1>
{% for post in posts %}
<article>
<h2>{{ post.title }}</h2>
<small>{{ post.date }}</small>
<p>{{ post.excerpt }}</p>
</article>
{% endfor %}
</body>
</html>
"#)?;
let app_state = AppState { template_engine };
let mut app = Application::new(config);
// Alternative: let mut router = Router::new(); (same API)
app.router_mut().get("/", {
let state = app_state.clone();
move |_| home_page(State(state))
});
app.router_mut().get("/blog", {
let state = app_state.clone();
move |_| blog_page(State(state))
});
app.run().await
}
Best Practices
- Organize Templates: Keep templates in a dedicated directory (usually
templates/) - Use Base Templates: Create base templates with common layout elements
- Context Management: Use structured context data rather than individual variables
- Error Handling: Always handle template rendering errors appropriately
- Caching: Consider implementing template caching for production applications
- Security: The template engine automatically escapes HTML to prevent XSS
Security Considerations
The Oxidite template engine includes built-in security features:
- Automatic HTML escaping to prevent XSS
- Context isolation between different template renders
- Input validation for template variables
Remember to always validate and sanitize user input before passing it to templates, especially when dealing with dynamic content.
Custom Filters
You can register custom filters to extend the template engine with project-specific transformations:
use oxidite_template::{TemplateEngine, Context};
let templates = TemplateContext::new("templates");
// Register a custom filter
engine.register_filter("shout", |s: &str| s.to_uppercase() + "!!!");
// Register a date formatting filter
engine.register_filter("short_date", |s: &str| {
// Your date formatting logic here
format!("Date: {}", s)
});
engine.add_template("greeting", "{{ name | shout }}")?;
let mut context = Context::new();
context.set("name", "hello");
let html = engine.render("greeting", &context)?;
assert_eq!(html, "HELLO!!!");
Custom filters are applied the same way as built-in filters using the pipe (|) syntax in templates.