feat: add minijinja as template engine

This commit is contained in:
Alexander Navarro 2025-02-14 11:29:03 -03:00
parent 5a9b871e42
commit 6cb75aa442
10 changed files with 150 additions and 31 deletions

21
Cargo.lock generated
View file

@ -125,6 +125,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"axum", "axum",
"minijinja", "minijinja",
"minijinja-embed",
"notify", "notify",
"serde", "serde",
"thiserror", "thiserror",
@ -387,6 +388,12 @@ version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "memo-map"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38d1115007560874e373613744c6fba374c17688327a71c1476d1a5954cc857b"
[[package]] [[package]]
name = "mime" name = "mime"
version = "0.3.17" version = "0.3.17"
@ -399,9 +406,17 @@ version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cff7b8df5e85e30b87c2b0b3f58ba3a87b68e133738bf512a7713769326dbca9" checksum = "cff7b8df5e85e30b87c2b0b3f58ba3a87b68e133738bf512a7713769326dbca9"
dependencies = [ dependencies = [
"memo-map",
"self_cell",
"serde", "serde",
] ]
[[package]]
name = "minijinja-embed"
version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff215cc12986bb95143f1af134415fa2554aacbb4f74416973549ac54ab0c907"
[[package]] [[package]]
name = "miniz_oxide" name = "miniz_oxide"
version = "0.8.4" version = "0.8.4"
@ -595,6 +610,12 @@ dependencies = [
"winapi-util", "winapi-util",
] ]
[[package]]
name = "self_cell"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d369a96f978623eb3dc28807c4852d6cc617fed53da5d3c400feff1ef34a714a"
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.217" version = "1.0.217"

View file

@ -5,12 +5,16 @@ edition = "2021"
[dependencies] [dependencies]
axum = "0.8.1" axum = "0.8.1"
minijinja = "2.7.0" minijinja = { version = "2.7.0", features = ["loader"] }
minijinja-embed = "2.7.0"
notify = "8.0.0" notify = "8.0.0"
serde = "1.0.217" serde = { version = "1.0.217", features = ["derive"] }
thiserror = "2.0.11" thiserror = "2.0.11"
tokio = { version = "1.43.0", features = ["macros", "rt-multi-thread"] } tokio = { version = "1.43.0", features = ["macros", "rt-multi-thread"] }
tower-http = { version = "0.6.2", features = ["trace"] } tower-http = { version = "0.6.2", features = ["trace"] }
tower-livereload = "0.9.6" tower-livereload = "0.9.6"
tracing = "0.1.41" tracing = "0.1.41"
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
[build-dependencies]
minijinja-embed = "2.7.0"

5
build.rs Normal file
View file

@ -0,0 +1,5 @@
fn main() {
// only enable in production build
#[cfg(not(debug_assertions))]
minijinja_embed::embed_templates!("templates");
}

View file

@ -1,5 +1,8 @@
use axum::response::Html; use axum::{
use axum::{http::StatusCode, response::IntoResponse}; http::StatusCode,
response::{Html, IntoResponse},
};
use tracing::debug;
pub type Result<T> = std::result::Result<T, Error>; pub type Result<T> = std::result::Result<T, Error>;
@ -34,6 +37,8 @@ impl IntoResponse for Error {
), ),
}; };
debug!(error = ?self);
(status, message).into_response() (status, message).into_response()
} }
} }

View file

@ -1,3 +1,28 @@
mod error; mod error;
pub mod router;
pub use error::{Error, Result, ResultTemplate}; pub use error::{Error, Result, ResultTemplate};
use minijinja::{Environment, Template};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
#[derive(Serialize, Deserialize, Debug)]
pub struct Link {
pub path: String,
pub text: String,
pub subpages: Vec<Self>,
}
pub struct AppState {
tmpl_env: Environment<'static>,
}
impl AppState {
pub fn new(tmpl_env: Environment<'static>) -> Arc<Self> {
Arc::new(AppState { tmpl_env })
}
pub fn get_template(&self, name: &str) -> Result<Template> {
Ok(self.tmpl_env.get_template(name)?)
}
}

View file

@ -1,21 +1,31 @@
#![allow(unused)] #![allow(unused)]
#![allow(dead_code)] #![allow(dead_code)]
use compendium::{router, AppState, Link, Result};
use minijinja::{Environment, Value};
use std::sync::Arc; use std::sync::Arc;
use axum::extract::State;
use axum::http::StatusCode;
use axum::response::Html;
use axum::routing::get;
use axum::Router;
use compendium::{Result, ResultTemplate};
use minijinja::{context, Environment};
use tower_http::trace::TraceLayer; use tower_http::trace::TraceLayer;
use tracing::info; use tracing::info;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
struct AppState { fn load_templates() -> Result<Environment<'static>> {
tmpl_env: Environment<'static>, let mut tmpl_env = Environment::new();
#[cfg(debug_assertions)]
tmpl_env.set_loader(minijinja::path_loader("templates")); // Path is relative to project root
// only enable in production build
#[cfg(not(debug_assertions))]
minijinja_embed::load_templates!(&mut env);
let global_routes = vec![Link {
path: "/about".to_owned(),
text: "About".to_owned(),
subpages: vec![],
}];
tmpl_env.add_global("global_routes", Value::from_serialize(&global_routes));
Ok(tmpl_env)
} }
#[tokio::main] #[tokio::main]
@ -30,15 +40,11 @@ async fn main() -> Result<()> {
.with(tracing_subscriber::fmt::layer()) .with(tracing_subscriber::fmt::layer())
.init(); .init();
let mut tmpl_env = Environment::new(); let mut tmpl_env = load_templates()?;
tmpl_env.add_template("base", include_str!("../templates/base.html"))?;
let app_state = Arc::new(AppState { tmpl_env }); let app = router::new()
let app = Router::new()
.route("/", get(handler_home))
.layer(TraceLayer::new_for_http().on_request(())) .layer(TraceLayer::new_for_http().on_request(()))
.with_state(app_state); .with_state(AppState::new(tmpl_env));
// Add hot reload only on dev mode // Add hot reload only on dev mode
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
@ -50,11 +56,3 @@ async fn main() -> Result<()> {
axum::serve(listener, app).await?; axum::serve(listener, app).await?;
Ok(()) Ok(())
} }
async fn handler_home(State(state): State<Arc<AppState>>) -> ResultTemplate {
let template = state.tmpl_env.get_template("base")?;
let content = template.render(context!())?;
Ok(Html(content))
}

17
src/router.rs Normal file
View file

@ -0,0 +1,17 @@
use axum::{extract::State, response::Html, routing::get, Router};
use minijinja::context;
use std::sync::Arc;
use crate::{AppState, ResultTemplate};
pub fn new() -> Router<Arc<AppState>> {
Router::new().route("/", get(handler_home))
}
async fn handler_home(State(state): State<Arc<AppState>>) -> ResultTemplate {
let template = state.tmpl_env.get_template("base.html")?;
let content = template.render(context!())?;
Ok(Html(content))
}

View file

@ -2,9 +2,19 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Compendium</title> <title>
{% block title %}Axum web service!{% endblock %}
</title>
</head> </head>
<body> <body>
<h1>Hello world!</h1> {% include "partials/header.html" %}
<main>
<nav class="msp-d-sm-none msp-d-flex msp-justify-content-end">
<button class="msp-offcanvas-toggle" data-msp-target="#main-offcanvas">Toggle</button>
</nav>
{% block content %}{% endblock %}
</main>
</body> </body>
</html> </html>

0
templates/index.html Normal file
View file

View file

@ -0,0 +1,34 @@
<aside id="main-offcanvas" class="msp-offcanvas msp-offcanvas-sm">
<div class="msp-offcanvas-backdrop msp-offcanvas-toggle"
data-msp-target="#main-offcanvas"></div>
<div class="msp-offcanvas-content">
<div class="msp-offcanvas-body">
<ul class="msp-list-unstyle msp-accordion">
<li>
<a href="/">Overview</a>
</li>
{% for page in global_routes %}
<li class="msp-accordion-item">
<a class="msp-accordion-header" href="{{ page.path }}">{{ page.text }}</a>
{% if page.subpages.length > 0 %}
}
<div class="msp-accordion-collapse">
<ul class="msp-list-unstyle msp-accordion-content">
{% for subpage in page.subpages %}
<li class="">
<a href="{{ subpage.path }}">{{ subpage.text }}</a>
</li>
{% endfor %}
</ul>
</div>
{
{% endif %}
</li>
{% endfor %}
</ul>
</div>
</div>
</aside>