Templates
Templates
Blixt uses Askama for compile-time checked HTML templates. Templates are type-safe Rust structs that map to HTML files -- if a template variable is missing or the wrong type, the compiler catches it.
Defining a template
Derive Template on a struct and point it at an HTML file:
use blixt::prelude::*;
#[derive(Template)]
#[template(path = "pages/home.html")]
struct HomePage {
title: String,
posts: Vec<Post>,
}
The path is relative to your project's templates/ directory. Fields on the struct become variables in the template.
Template file locations
Generated Blixt projects organize templates into subdirectories by purpose:
templates/
layouts/ # Base layouts with shared HTML structure
pages/ # Full page templates that extend layouts
fragments/ # Partial HTML for Datastar SSE updates
components/ # Reusable UI components
emails/ # Email body templates
This is a convention, not a requirement -- Askama resolves any path relative to templates/.
Template inheritance
Layouts define a base HTML structure with named blocks. Pages extend a layout and fill in those blocks.
Layout (templates/layouts/app.html):
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="{% block description %}A Blixt application{% endblock %}">
<title>{% block title %}Blixt App{% endblock %}</title>
<link rel="stylesheet" href="/static/css/output.css" data-blixt-css>
</head>
<body>
{% block content %}{% endblock %}
<script type="module" src="/static/js/datastar.js"></script>
</body>
</html>
Page (templates/pages/home.html):
{% extends "layouts/app.html" %}
{% block title %}Home{% endblock %}
{% block description %}Welcome to the app{% endblock %}
{% block content %}
<main>
<h1>{{ greeting }}</h1>
</main>
{% endblock %}
Blocks not overridden keep their default content from the layout.
The render! macro
render! takes a template struct, calls .render(), and wraps the result in Html for Axum. It converts rendering errors into Error::Internal.
async fn index() -> Result<impl IntoResponse> {
render!(HomePage {
title: "Welcome".to_string(),
posts: vec![],
})
}
This expands to roughly:
let html = template.render().map_err(|e| Error::Internal(e.to_string()))?;
Ok(Html(html))
Without the macro you would write:
async fn index() -> Result<impl IntoResponse> {
let page = HomePage { title: "Welcome".to_string(), posts: vec![] };
let html = page.render().map_err(|e| Error::Internal(e.to_string()))?;
Ok(Html(html))
}
Including fragments
Use {% include %} to embed one template inside another. This is how list and item fragments compose:
List fragment (templates/fragments/todo_list.html):
<div id="todo-list">
{% for todo in page.items %}
{% include "fragments/todo_item.html" %}
{% endfor %}
</div>
Page including the fragment (templates/pages/home.html):
{% extends "layouts/app.html" %}
{% block content %}
<main>
{% include "fragments/todo_list.html" %}
</main>
{% endblock %}
Included templates have access to all variables in the including template's scope. The todo variable from the {% for %} loop is available inside todo_item.html.
Template syntax
Askama uses {{ }} for expressions and {% %} for control flow:
<!-- Variables -->
<h1>{{ title }}</h1>
<!-- Conditionals -->
{% if page.items.is_empty() %}
<p>No items yet.</p>
{% else %}
<p>{{ page.total }} items found.</p>
{% endif %}
<!-- Loops -->
{% for item in items %}
<li>{{ item.name }}</li>
{% endfor %}
<!-- Method calls -->
<span>Page {{ page.page }} / {{ page.total_pages }}</span>
<!-- Expressions -->
{% if page.total == 1 %}item{% else %}items{% endif %}
Auto-escaping
Askama escapes HTML by default in .html templates. Given a struct field containing <script>alert('xss')</script>, the output will be:
<script>alert('xss')</script>
This prevents XSS without any extra work. To render raw HTML (only for trusted content), use the |safe filter:
{{ trusted_html|safe }}
Fragments for Datastar
Fragments are partial HTML templates used with Datastar SSE responses. Define them as standalone template structs:
#[derive(Template)]
#[template(path = "fragments/todo_list.html")]
struct TodoListFragment {
page: Paginated<Todo>,
}
Return them as SSE patches using SseFragment or SseResponse:
async fn update(State(ctx): State<AppContext>) -> Result<impl IntoResponse> {
let page = fetch_page(&ctx.db, 1).await?;
SseFragment::new(TodoListFragment { page })
}
The fragment HTML replaces the matching DOM element (by id attribute) on the client, with no full page reload.