在 Rust Web 项目里渲染 HTML,有人用 askama(编译期类型检查),有人用 Minijinja(Jinja2 语法、运行时灵活)。博客、管理后台、邮件模板这类「结构多变、热更新友好」的场景,Minijinja 往往更顺手。

基本集成

[dependencies]
minijinja = { version = "2", features = ["loader"] }
use minijinja::{context, Environment};

fn render_index(posts: &[Post]) -> String {
let mut env = Environment::new();
env.add_template("index.html", include_str!("../templates/index.html"))
.unwrap();
let tpl = env.get_template("index.html").unwrap();
tpl.render(context! {
posts => posts,
site => site_config(),
}).unwrap()
}

include_str! 把模板编进二进制,部署简单;开发期也可改成从 templates/ 目录加载方便热重载。

与 Jinja2 相通的语法

{% for post in posts %}
<article>
<h2><a href="{{ post.url }}">{{ post.title }}</a></h2>
<p>{{ post.excerpt }}</p>
</article>
{% else %}
<p>暂无文章</p>
{% endfor %}

{% if filter_category %}
<span>当前分类:{{ filter_category }}</span>
{% endif %}

前端同学如果熟悉 Flask/Django,几乎零学习成本。

include 拆分公共片段

{% include "head_meta.html" %}
{% include "admin_nav.html" %}

导航、页脚、meta 标签各一份,管理后台六个页面共用 admin_nav.html,改一处全局生效——这也是「菜单栏不跳」的基础。

自定义过滤器

URL 编码、日期格式化常在 Rust 侧注册:

env.add_filter("urlencode", |s: String| -> String {
urlencoding::encode(&s).into_owned()
});

模板里:{{ post.category | urlencode }}

复杂逻辑放 Rust,模板只做展示,边界清晰。

管理页 vs 博客页

类型数据来源模板特点
博客首页内存中的 Post 列表循环、分页、侧栏
文章详情单篇 HTML 正文{{ post.content_html | safe }}
管理后台API + 少量 SSR表单、表格、统一 layout

注意:safe 只对可信 HTML 使用(你自己 Markdown 渲染出来的),用户评论内容必须转义。

错误处理

pub fn render(env: &Environment, name: &str, ctx: Value) -> String {
env.get_template(name)
.and_then(|t| t.render(ctx))
.unwrap_or_else(|e| {
tracing::error!("template {name}: {e}");
"页面渲染失败".into()
})
}

生产环境不要 unwrap() 把堆栈吐给用户;记日志 + 友好 500 页。

和纯前端的取舍

  • SSR 模板:首屏快、SEO 友好、后台页简单
  • SPA:交互复杂时更合适

个人博客「文章列表 + 详情 + 轻量后台」,SSR + 少量 fetch(评论、分页)通常是甜点位。

小结

Minijinja 的价值在于:Rust 的类型与性能 + 模板语言的表达力。把公共片段拆好、过滤器放在 Rust、危险 HTML 管住,就能用很少代码维护一整套站点与管理界面。

如果你已经在用 Axum,加上 Minijinja 往往就是再加一个 Environment 和几份 HTML——比重构到 React 便宜得多。