Rust ＋ Actix-web でのHTMLのテンプレートエンジンについて

## 前書き
RUST + Actix-web の開発において、何らかのHTMLテンプレートをレンダーするテンプレートエンジンを探していたのですが、[Actix-webの実装例をまとめた公式サイト(actix/examples)](https://github.com/actix/examples)を見ても複数出てきて情報が過多だったので、ざっくり情報を集めてみた際の備忘録 (2023/11/30時点での情報) になります。

## 比較対象
[actix/examples のリポジトリの /templating/ 配下](https://github.com/actix/examples/tree/master/templating)にあるものを候補としてます。

- [Askama](https://github.com/djc/askama)
- [Fluent Templates](https://github.com/XAMPPRocky/fluent-templates)
- [handlebars-rust](https://github.com/sunng87/handlebars-rust)
- [MiniJinja](https://github.com/mitsuhiko/minijinja)
- [SailFish](https://github.com/rust-sailfish/sailfish)
- [Tera](https://github.com/Keats/tera)
- [TinyTemplate](https://github.com/bheisler/TinyTemplate)
- [Yarte](https://github.com/zzau13/yarte)

### 比較対象にしなかったもの
下記は[actix/examples のリポジトリの /templating/ 配下](https://github.com/actix/examples/tree/master/templating)になかったので今回は非対象にしました。
- [Horrorshow](https://github.com/Stebalien/horrorshow-rs)
- [Rust Compiled Templates — ructe](https://github.com/kaj/ructe)
- [markup.rs](https://github.com/utkarshkukreti/markup.rs)

## Github のリポジトリベースでの比較 (確認日: 2023/11/30)
Githubのリポジトリの情報でざっくり比較すると下記になりました。
各列の上位2件まで色を緑に変更しております。

|名称|Star|Fork|Issue<br>(Open)|Issue<br>(Close)|最新Tag|1つ前のマイナーTag|URL|
|:--|:---|:---|:--------------|:---------------|:-----|:-------------------|:---|
|Askama|<font color="Lime">2.7k</font>|<font color="Lime">191</font>|<font color="Lime">97</font>|249|0.12.0<br>(2023/03/06)|0.11.0<br>(2021/12/22)|https://github.com/djc/askama|
|Fluent Templates|122|17|12|22|v0.8.0<br>(2022/08/08)|v0.7.1<br>(2022/03/16)| https://github.com/XAMPPRocky/fluent-templates|
|handlebars-rust|1.1k|132|25|<font color="Lime">278</font>|<font color="Lime">v4.5.0<br>(2023/11/09)</font>|<font color="Lime">v4.4.0<br>(2023/09/01)</font>|https://github.com/sunng87/handlebars-rust|
|MiniJinja|1.1k|72|4|114|<font color="Lime">1.0.10<br>(2023/11/10)</font>|0.34.0<br>(2023/06/02)|https://github.com/mitsuhiko/minijinja|
|SailFish|624|50|30|49|v0.8.1<br>(2023/10/10)|<font color="Lime">v0.7.0<br>(2023/06/23)</font>|https://github.com/rust-sailfish/sailfish|
|Tera|<font color="Lime">3k</font>|<font color="Lime">254</font>|<font color="Lime">141</font>|<font color="Lime">378</font>|v1.18.1<br>(2023/03/16)|v1.17.1<br>(2022/09/19)|https://github.com/Keats/tera|
|TinyTemplate|176|42|12|13|1.2.1<br>(2021/03/04)|1.1.0<br>(2020/06/01)|https://github.com/bheisler/TinyTemplate|
|Yarte|265|16|29|69|yarte-v0.15.7<br>(2022/09/07)|yarte-v0.15.6<br>(2022/09/07) ※1 |https://github.com/zzau13/yarte|

※1 直近のマイナーバージョンのTagが存在しないので、もっとも古いTagを記載。

## 実装の例
[actix/examples のリポジトリの /templating/ 配下](https://github.com/actix/examples/tree/master/templating)にある main.rs を転記しています。
詳細な情報については[actix/examples のリポジトリの /templating/ 配下](https://github.com/actix/examples/tree/master/templating)をご確認ください。


### Askama
<details><summary>Askamaのコード例(※クリックで展開)</summary><div>

```rust
use std::collections::HashMap;

use actix_web::{middleware, web, App, HttpServer, Responder, Result};
use actix_web_lab::respond::Html;
use askama::Template;

#[derive(Template)]
#[template(path = "user.html")]
struct UserTemplate<'a> {
    name: &'a str,
    text: &'a str,
}

#[derive(Template)]
#[template(path = "index.html")]
struct Index;

async fn index(query: web::Query<HashMap<String, String>>) -> Result<impl Responder> {
    let html = if let Some(name) = query.get("name") {
        UserTemplate {
            name,
            text: "Welcome!",
        }
        .render()
        .expect("template should be valid")
    } else {
        Index.render().expect("template should be valid")
    };

    Ok(Html(html))
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));

    log::info!("starting HTTP server at http://localhost:8080");

    HttpServer::new(move || {
        App::new()
            .wrap(middleware::Logger::default())
            .service(web::resource("/").route(web::get().to(index)))
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}
```

</div></details>

source: https://github.com/actix/examples/blob/master/templating/askama/src/main.rs

### Fluent Templates
<details><summary>Fluent Templatesのコード例(※クリックで展開)</summary><div>

```rust
use std::io;

use actix_web::{
    body::BoxBody,
    dev::ServiceResponse,
    get,
    http::{header::ContentType, StatusCode},
    middleware::{ErrorHandlerResponse, ErrorHandlers},
    web, App, HttpResponse, HttpServer, Responder, Result,
};
use actix_web_lab::{extract::Path, respond::Html};
use fluent_templates::{static_loader, FluentLoader, Loader as _};
use handlebars::Handlebars;
use serde_json::json;

mod lang_choice;
use self::lang_choice::LangChoice;

static_loader! {
    static LOCALES = {
        locales: "./locales",
        fallback_language: "en",

        // removes unicode isolating marks around arguments
        // you typically should only set to false when testing.
        customise: |bundle| bundle.set_use_isolating(false),
    };
}

#[get("/")]
async fn index(hb: web::Data<Handlebars<'_>>, lang: LangChoice) -> impl Responder {
    let data = json!({ "lang": lang });
    let body = hb.render("index", &data).unwrap();
    Html(body)
}

#[get("/{user}/{data}")]
async fn user(
    hb: web::Data<Handlebars<'_>>,
    Path(info): Path<(String, String)>,
    lang: LangChoice,
) -> impl Responder {
    let data = json!({
        "lang": lang,
        "user": info.0,
        "data": info.1
    });
    let body = hb.render("user", &data).unwrap();
    Html(body)
}

#[actix_web::main]
async fn main() -> io::Result<()> {
    // Handlebars uses a repository for the compiled templates. This object must be shared between
    // the application threads, and is therefore passed to the App in an Arc.
    let mut handlebars = Handlebars::new();

    // register template dir with Handlebars registry
    handlebars
        .register_templates_directory(".html", "./templates")
        .unwrap();

    // register Fluent helper with Handlebars registry
    handlebars.register_helper("fluent", Box::new(FluentLoader::new(&*LOCALES)));

    let handlebars = web::Data::new(handlebars);

    HttpServer::new(move || {
        App::new()
            .wrap(error_handlers())
            .app_data(web::Data::clone(&handlebars))
            .service(index)
            .service(user)
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}

// Custom error handlers, to return HTML responses when an error occurs.
fn error_handlers() -> ErrorHandlers<BoxBody> {
    ErrorHandlers::new().handler(StatusCode::NOT_FOUND, not_found)
}

// Error handler for a 404 Page not found error.
fn not_found<B>(res: ServiceResponse<B>) -> Result<ErrorHandlerResponse<BoxBody>> {
    let lang = LangChoice::from_req(res.request()).lang_id();
    let error = LOCALES.lookup(&lang, "error-not-found").unwrap();

    let response = get_error_response(&res, &error);

    Ok(ErrorHandlerResponse::Response(ServiceResponse::new(
        res.into_parts().0,
        response.map_into_left_body(),
    )))
}

// Generic error handler.
fn get_error_response<B>(res: &ServiceResponse<B>, error: &str) -> HttpResponse<BoxBody> {
    let req = res.request();
    let lang = LangChoice::from_req(req);

    // Provide a fallback to a simple plain text response in case an error occurs during the
    // rendering of the error page.

    let hb = req
        .app_data::<web::Data<Handlebars>>()
        .expect("correctly set up handlebars in app data");

    let data = json!({
        "lang": lang,
        "error": error,
        "status_code": res.status().as_str()
    });

    let body = hb.render("error", &data);

    match body {
        Ok(body) => HttpResponse::build(res.status())
            .content_type(ContentType::html())
            .body(body),

        Err(_) => HttpResponse::build(res.status())
            .content_type(ContentType::plaintext())
            .body(error.to_string()),
    }
}
```

</div></details>

source: https://github.com/actix/examples/blob/master/templating/fluent/src/main.rs

### handlebars-rust
<details><summary>handlebars-rustのコード例(※クリックで展開)</summary><div>

```rust
use std::io;

use actix_web::{
    body::BoxBody,
    dev::ServiceResponse,
    get,
    http::{header::ContentType, StatusCode},
    middleware::{ErrorHandlerResponse, ErrorHandlers},
    web, App, HttpResponse, HttpServer, Result,
};
use handlebars::Handlebars;
use serde_json::json;

// Macro documentation can be found in the actix_web_codegen crate
#[get("/")]
async fn index(hb: web::Data<Handlebars<'_>>) -> HttpResponse {
    let data = json!({
        "name": "Handlebars"
    });
    let body = hb.render("index", &data).unwrap();

    HttpResponse::Ok().body(body)
}

#[get("/{user}/{data}")]
async fn user(hb: web::Data<Handlebars<'_>>, path: web::Path<(String, String)>) -> HttpResponse {
    let info = path.into_inner();
    let data = json!({
        "user": info.0,
        "data": info.1
    });
    let body = hb.render("user", &data).unwrap();

    HttpResponse::Ok().body(body)
}

#[actix_web::main]
async fn main() -> io::Result<()> {
    // Handlebars uses a repository for the compiled templates. This object must be
    // shared between the application threads, and is therefore passed to the
    // Application Builder as an atomic reference-counted pointer.
    let mut handlebars = Handlebars::new();
    handlebars
        .register_templates_directory(".html", "./static/templates")
        .unwrap();
    let handlebars_ref = web::Data::new(handlebars);

    HttpServer::new(move || {
        App::new()
            .wrap(error_handlers())
            .app_data(handlebars_ref.clone())
            .service(index)
            .service(user)
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}

// Custom error handlers, to return HTML responses when an error occurs.
fn error_handlers() -> ErrorHandlers<BoxBody> {
    ErrorHandlers::new().handler(StatusCode::NOT_FOUND, not_found)
}

// Error handler for a 404 Page not found error.
fn not_found<B>(res: ServiceResponse<B>) -> Result<ErrorHandlerResponse<BoxBody>> {
    let response = get_error_response(&res, "Page not found");
    Ok(ErrorHandlerResponse::Response(ServiceResponse::new(
        res.into_parts().0,
        response.map_into_left_body(),
    )))
}

// Generic error handler.
fn get_error_response<B>(res: &ServiceResponse<B>, error: &str) -> HttpResponse<BoxBody> {
    let request = res.request();

    // Provide a fallback to a simple plain text response in case an error occurs during the
    // rendering of the error page.
    let fallback = |err: &str| {
        HttpResponse::build(res.status())
            .content_type(ContentType::plaintext())
            .body(err.to_string())
    };

    let hb = request
        .app_data::<web::Data<Handlebars>>()
        .map(|t| t.get_ref());
    match hb {
        Some(hb) => {
            let data = json!({
                "error": error,
                "status_code": res.status().as_str()
            });
            let body = hb.render("error", &data);

            match body {
                Ok(body) => HttpResponse::build(res.status())
                    .content_type(ContentType::html())
                    .body(body),
                Err(_) => fallback(error),
            }
        }
        None => fallback(error),
    }
}
```

</div></details>

source: https://github.com/actix/examples/blob/master/templating/handlebars/src/main.rs

### MiniJinja
<details><summary>MiniJinjaのコード例(※クリックで展開)</summary><div>

```rust
use std::{collections::HashMap, env, path::PathBuf};

use actix_utils::future::{ready, Ready};
use actix_web::{
    dev::{self, ServiceResponse},
    error,
    http::{header::ContentType, StatusCode},
    middleware::{ErrorHandlerResponse, ErrorHandlers, Logger},
    web, App, FromRequest, HttpRequest, HttpResponse, HttpServer, Responder, Result,
};
use actix_web_lab::respond::Html;
use minijinja::path_loader;
use minijinja_autoreload::AutoReloader;

struct MiniJinjaRenderer {
    tmpl_env: web::Data<minijinja_autoreload::AutoReloader>,
}

impl MiniJinjaRenderer {
    fn render(
        &self,
        tmpl: &str,
        ctx: impl Into<minijinja::value::Value>,
    ) -> actix_web::Result<Html> {
        self.tmpl_env
            .acquire_env()
            .map_err(|_| error::ErrorInternalServerError("could not acquire template env"))?
            .get_template(tmpl)
            .map_err(|_| error::ErrorInternalServerError("could not find template"))?
            .render(ctx.into())
            .map(Html)
            .map_err(|err| {
                log::error!("{err}");
                error::ErrorInternalServerError("template error")
            })
    }
}

impl FromRequest for MiniJinjaRenderer {
    type Error = actix_web::Error;
    type Future = Ready<Result<Self, Self::Error>>;

    fn from_request(req: &HttpRequest, _pl: &mut dev::Payload) -> Self::Future {
        let tmpl_env = <web::Data<minijinja_autoreload::AutoReloader>>::extract(req)
            .into_inner()
            .unwrap();

        ready(Ok(Self { tmpl_env }))
    }
}

async fn index(
    tmpl_env: MiniJinjaRenderer,
    query: web::Query<HashMap<String, String>>,
) -> actix_web::Result<impl Responder> {
    if let Some(name) = query.get("name") {
        tmpl_env.render(
            "user.html",
            minijinja::context! {
                name,
                text => "Welcome!",
            },
        )
    } else {
        tmpl_env.render("index.html", ())
    }
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));

    // If TEMPLATE_AUTORELOAD is set, then the path tracking is enabled.
    let enable_template_autoreload = env::var("TEMPLATE_AUTORELOAD").as_deref() == Ok("true");

    if enable_template_autoreload {
        log::info!("template auto-reloading is enabled");
    } else {
        log::info!(
            "template auto-reloading is disabled; run with TEMPLATE_AUTORELOAD=true to enable"
        );
    }

    // The closure is invoked every time the environment is outdated to recreate it.
    let tmpl_reloader = AutoReloader::new(move |notifier| {
        let mut env: minijinja::Environment<'static> = minijinja::Environment::new();

        let tmpl_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("templates");

        // if watch_path is never called, no fs watcher is created
        if enable_template_autoreload {
            notifier.watch_path(&tmpl_path, true);
        }

        env.set_loader(path_loader(tmpl_path));

        Ok(env)
    });

    let tmpl_reloader = web::Data::new(tmpl_reloader);

    log::info!("starting HTTP server at http://localhost:8080");

    HttpServer::new(move || {
        App::new()
            .app_data(tmpl_reloader.clone())
            .service(web::resource("/").route(web::get().to(index)))
            .wrap(ErrorHandlers::new().handler(StatusCode::NOT_FOUND, not_found))
            .wrap(Logger::default())
    })
    .workers(2)
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}

/// Error handler for a 404 Page not found error.
fn not_found<B>(svc_res: ServiceResponse<B>) -> Result<ErrorHandlerResponse<B>> {
    let res = get_error_response(&svc_res, "Page not found");

    Ok(ErrorHandlerResponse::Response(ServiceResponse::new(
        svc_res.into_parts().0,
        res.map_into_right_body(),
    )))
}

/// Generic error handler.
fn get_error_response<B>(res: &ServiceResponse<B>, error: &str) -> HttpResponse {
    let req = res.request();

    let tmpl_env = MiniJinjaRenderer::extract(req).into_inner().unwrap();

    // Provide a fallback to a simple plain text response in case an error occurs during the
    // rendering of the error page.
    let fallback = |err: &str| {
        HttpResponse::build(res.status())
            .content_type(ContentType::plaintext())
            .body(err.to_string())
    };

    let ctx = minijinja::context! {
        error => error,
        status_code => res.status().as_str(),
    };

    match tmpl_env.render("error.html", ctx) {
        Ok(body) => body
            .customize()
            .with_status(res.status())
            .respond_to(req)
            .map_into_boxed_body(),

        Err(_) => fallback(error),
    }
}
```

</div></details>

source: https://github.com/actix/examples/blob/master/templating/minijinja/src/main.rs

### SailFish
<details><summary>SailFishのコード例(※クリックで展開)</summary><div>

```rust
use actix_web::{
    error, get,
    middleware::{Compress, Logger},
    web, App, HttpServer, Responder,
};
use actix_web_lab::respond::Html;
use sailfish::TemplateOnce;

#[derive(TemplateOnce)]
#[template(path = "actix.stpl")]
struct Greet<'a> {
    name: &'a str,
}

#[derive(TemplateOnce)]
#[template(path = "page.stpl")]
struct Page<'a> {
    id: &'a i32,
}

#[get("/{name}")]
async fn greet(params: web::Path<(String,)>) -> actix_web::Result<impl Responder> {
    let body = Greet { name: &params.0 }
        .render_once()
        .map_err(error::ErrorInternalServerError)?;

    Ok(Html(body))
}

#[get("/page-{id:\\d+}")]
async fn page(params: web::Path<(i32,)>) -> actix_web::Result<impl Responder> {
    let body = Page { id: &params.0 }
        .render_once()
        .map_err(error::ErrorInternalServerError)?;

    Ok(Html(body))
}

#[get("/")]
async fn hello() -> impl Responder {
    Html("<p>Hello world!</p>".to_owned())
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));

    log::info!("starting HTTP server at http://localhost:8080");

    HttpServer::new(|| {
        App::new()
            .service(hello)
            .service(page)
            .service(greet)
            .wrap(Compress::default())
            .wrap(Logger::default())
    })
    .bind(("127.0.0.1", 8080))?
    .workers(1)
    .run()
    .await
}
```

</div></details>

source: https://github.com/actix/examples/blob/master/templating/sailfish/src/main.rs

### Tera
<details><summary>Teraのコード例(※クリックで展開)</summary><div>

```rust
use std::collections::HashMap;

use actix_web::{
    body::BoxBody,
    dev::ServiceResponse,
    error,
    http::{header::ContentType, StatusCode},
    middleware::{self, ErrorHandlerResponse, ErrorHandlers},
    web, App, Error, HttpResponse, HttpServer, Responder, Result,
};
use actix_web_lab::respond::Html;
use tera::Tera;

// store tera template in application state
async fn index(
    tmpl: web::Data<tera::Tera>,
    query: web::Query<HashMap<String, String>>,
) -> Result<impl Responder, Error> {
    let s = if let Some(name) = query.get("name") {
        // submitted form
        let mut ctx = tera::Context::new();
        ctx.insert("name", name);
        ctx.insert("text", "Welcome!");
        tmpl.render("user.html", &ctx)
            .map_err(|_| error::ErrorInternalServerError("Template error"))?
    } else {
        tmpl.render("index.html", &tera::Context::new())
            .map_err(|_| error::ErrorInternalServerError("Template error"))?
    };

    Ok(Html(s))
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));

    log::info!("starting HTTP server at http://localhost:8080");

    HttpServer::new(|| {
        let tera = Tera::new(concat!(env!("CARGO_MANIFEST_DIR"), "/templates/**/*")).unwrap();

        App::new()
            .app_data(web::Data::new(tera))
            .wrap(middleware::Logger::default())
            .service(web::resource("/").route(web::get().to(index)))
            .service(web::scope("").wrap(error_handlers()))
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}

// Custom error handlers, to return HTML responses when an error occurs.
fn error_handlers() -> ErrorHandlers<BoxBody> {
    ErrorHandlers::new().handler(StatusCode::NOT_FOUND, not_found)
}

// Error handler for a 404 Page not found error.
fn not_found<B>(res: ServiceResponse<B>) -> Result<ErrorHandlerResponse<BoxBody>> {
    let response = get_error_response(&res, "Page not found");
    Ok(ErrorHandlerResponse::Response(ServiceResponse::new(
        res.into_parts().0,
        response.map_into_left_body(),
    )))
}

// Generic error handler.
fn get_error_response<B>(res: &ServiceResponse<B>, error: &str) -> HttpResponse {
    let request = res.request();

    // Provide a fallback to a simple plain text response in case an error occurs during the
    // rendering of the error page.
    let fallback = |err: &str| {
        HttpResponse::build(res.status())
            .content_type(ContentType::plaintext())
            .body(err.to_string())
    };

    let tera = request.app_data::<web::Data<Tera>>().map(|t| t.get_ref());
    match tera {
        Some(tera) => {
            let mut context = tera::Context::new();
            context.insert("error", error);
            context.insert("status_code", res.status().as_str());
            let body = tera.render("error.html", &context);

            match body {
                Ok(body) => HttpResponse::build(res.status())
                    .content_type(ContentType::html())
                    .body(body),
                Err(_) => fallback(error),
            }
        }
        None => fallback(error),
    }
}
```

</div></details>

source: https://github.com/actix/examples/blob/master/templating/tera/src/main.rs

### TinyTemplate
<details><summary>TinyTemplateのコード例(※クリックで展開)</summary><div>

```rust
use std::collections::HashMap;

use actix_web::{
    body::BoxBody,
    dev::ServiceResponse,
    error,
    http::{header::ContentType, StatusCode},
    middleware,
    middleware::{ErrorHandlerResponse, ErrorHandlers},
    web, App, Error, HttpResponse, HttpServer, Result,
};
use serde_json::json;
use tinytemplate::TinyTemplate;

// store tiny_template in application state
async fn index(
    tmpl: web::Data<TinyTemplate<'_>>,
    query: web::Query<HashMap<String, String>>,
) -> Result<HttpResponse, Error> {
    let s = if let Some(name) = query.get("name") {
        // submitted form
        let ctx = json!({
          "name" : name.to_owned(),
          "text" : "Welcome!".to_owned()
        });
        tmpl.render("user.html", &ctx)
            .map_err(|_| error::ErrorInternalServerError("Template error"))?
    } else {
        tmpl.render("index.html", &serde_json::Value::Null)
            .map_err(|_| error::ErrorInternalServerError("Template error"))?
    };
    Ok(HttpResponse::Ok().content_type("text/html").body(s))
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));

    log::info!("starting HTTP server at http://localhost:8080");

    HttpServer::new(|| {
        let mut tt = TinyTemplate::new();
        tt.add_template("index.html", INDEX).unwrap();
        tt.add_template("user.html", USER).unwrap();
        tt.add_template("error.html", ERROR).unwrap();

        App::new()
            .app_data(web::Data::new(tt))
            .wrap(middleware::Logger::default())
            .service(web::resource("/").route(web::get().to(index)))
            .service(web::scope("").wrap(error_handlers()))
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}

// Custom error handlers, to return HTML responses when an error occurs.
fn error_handlers() -> ErrorHandlers<BoxBody> {
    ErrorHandlers::new().handler(StatusCode::NOT_FOUND, not_found)
}

// Error handler for a 404 Page not found error.
fn not_found<B>(res: ServiceResponse<B>) -> Result<ErrorHandlerResponse<BoxBody>> {
    let response = get_error_response(&res, "Page not found");
    Ok(ErrorHandlerResponse::Response(ServiceResponse::new(
        res.into_parts().0,
        response.map_into_left_body(),
    )))
}

// Generic error handler.
fn get_error_response<B>(res: &ServiceResponse<B>, error: &str) -> HttpResponse {
    let request = res.request();

    // Provide a fallback to a simple plain text response in case an error occurs during the
    // rendering of the error page.
    let fallback = |err: &str| {
        HttpResponse::build(res.status())
            .content_type(ContentType::plaintext())
            .body(err.to_string())
    };

    let tt = request
        .app_data::<web::Data<TinyTemplate<'_>>>()
        .map(|t| t.get_ref());
    match tt {
        Some(tt) => {
            let mut context = std::collections::HashMap::new();
            context.insert("error", error.to_owned());
            context.insert("status_code", res.status().as_str().to_owned());
            let body = tt.render("error.html", &context);

            match body {
                Ok(body) => HttpResponse::build(res.status())
                    .content_type(ContentType::html())
                    .body(body),
                Err(_) => fallback(error),
            }
        }
        None => fallback(error),
    }
}

static ERROR: &str = include_str!("../templates/error.html");
static INDEX: &str = include_str!("../templates/index.html");
static USER: &str = include_str!("../templates/user.html");
```

</div></details>

source: https://github.com/actix/examples/blob/master/templating/tinytemplate/src/main.rs

### Yarte
<details><summary>Yarteのコード例(※クリックで展開)</summary><div>

```rust
use std::collections::HashMap;

use actix_web::{
    get, middleware::Logger, web, App, Error, HttpResponse, HttpServer, ResponseError,
};
use derive_more::Display;
use yarte::{auto, ywrite_min};

#[derive(Debug, Display)]
struct MyErr(pub &'static str);

impl ResponseError for MyErr {}

#[allow(unused_must_use)] // ywrite_min causes warning: unused borrow that must be used
#[get("/")]
async fn index(query: web::Query<HashMap<String, String>>) -> Result<HttpResponse, Error> {
    // `ywrite_min` is work in progress check your templates before put in production
    // or use `ywrite_html`
    Ok(HttpResponse::Ok()
        .content_type("text/html; charset=utf-8")
        .body(auto!(ywrite_min!(String, "{{> index }}"))))
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));

    log::info!("starting HTTP server at http://localhost:8080");

    HttpServer::new(move || App::new().wrap(Logger::default()).service(index))
        .bind(("127.0.0.1", 8080))?
        .run()
        .await
}

#[cfg(test)]
mod test {
    use actix_web::{http, test as atest, web::Bytes};

    use super::*;

    #[actix_web::test]
    async fn test() {
        let app = atest::init_service(App::new().service(index)).await;

        let req = atest::TestRequest::with_uri("/").to_request();
        let resp = atest::call_service(&app, req).await;

        assert!(resp.status().is_success());

        assert_eq!(
            resp.headers().get(http::header::CONTENT_TYPE).unwrap(),
            "text/html; charset=utf-8"
        );

        let bytes = atest::read_body(resp).await;
        assert_eq!(
            bytes,
            Bytes::from_static(
                "<!DOCTYPE html><html><head><meta charset=\"utf-8\"><title>Actix \
                 Web</title></head><body><h1 id=\"welcome\" \
                 class=\"welcome\">Welcome!</h1><div><h3>What is your name?</h3><form>Name: \
                 <input type=\"text\" name=\"name\"><br>Last name: <input type=\"text\" \
                 name=\"lastname\"><br><p><input type=\"submit\"></p></form></div></body></html>"
                    .as_ref()
            )
        );

        let req = atest::TestRequest::with_uri("/?name=foo&lastname=bar").to_request();
        let resp = atest::call_service(&app, req).await;

        assert!(resp.status().is_success());

        assert_eq!(
            resp.headers().get(http::header::CONTENT_TYPE).unwrap(),
            "text/html; charset=utf-8"
        );

        let bytes = atest::read_body(resp).await;
        assert_eq!(
            bytes,
            Bytes::from_static(
                "<!DOCTYPE html><html><head><meta charset=\"utf-8\"><title>Actix \
                 Web</title></head><body><h1>Hi, foo bar!</h1><p id=\"hi\" \
                 class=\"welcome\">Welcome</p></body></html>"
                    .as_ref()
            )
        );

        let req = atest::TestRequest::with_uri("/?name=foo").to_request();
        let resp = atest::call_service(&app, req).await;

        assert!(resp.status().is_server_error());

        let bytes = atest::read_body(resp).await;

        assert_eq!(bytes, Bytes::from_static("Bad query".as_ref()));

        let req = atest::TestRequest::with_uri("/?lastname=bar").to_request();
        let resp = atest::call_service(&app, req).await;

        assert!(resp.status().is_success());

        assert_eq!(
            resp.headers().get(http::header::CONTENT_TYPE).unwrap(),
            "text/html; charset=utf-8"
        );

        let bytes = atest::read_body(resp).await;
        assert_eq!(
            bytes,
            Bytes::from_static(
                "<!DOCTYPE html><html><head><meta charset=\"utf-8\"><title>Actix \
                 Web</title></head><body><h1 id=\"welcome\" \
                 class=\"welcome\">Welcome!</h1><div><h3>What is your name?</h3><form>Name: \
                 <input type=\"text\" name=\"name\"><br>Last name: <input type=\"text\" \
                 name=\"lastname\"><br><p><input type=\"submit\"></p></form></div></body></html>"
                    .as_ref()
            )
        );
    }
}
```

</div></details>

source: https://github.com/actix/examples/blob/master/templating/yarte/src/main.rs