A little app to serve my photography from my personal website

dynamic templates

+69 -6
+14
Cargo.lock
··· 834 834 checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" 835 835 836 836 [[package]] 837 + name = "memo-map" 838 + version = "0.3.3" 839 + source = "registry+https://github.com/rust-lang/crates.io-index" 840 + checksum = "38d1115007560874e373613744c6fba374c17688327a71c1476d1a5954cc857b" 841 + 842 + [[package]] 837 843 name = "mime" 838 844 version = "0.3.17" 839 845 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 855 861 source = "registry+https://github.com/rust-lang/crates.io-index" 856 862 checksum = "a9f264d75233323f4b7d2f03aefe8a990690cdebfbfe26ea86bcbaec5e9ac990" 857 863 dependencies = [ 864 + "memo-map", 865 + "self_cell", 858 866 "serde", 859 867 ] 860 868 ··· 1149 1157 version = "1.2.0" 1150 1158 source = "registry+https://github.com/rust-lang/crates.io-index" 1151 1159 checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 1160 + 1161 + [[package]] 1162 + name = "self_cell" 1163 + version = "1.2.0" 1164 + source = "registry+https://github.com/rust-lang/crates.io-index" 1165 + checksum = "0f7d95a54511e0c7be3f51e8867aa8cf35148d7b9445d44de2f943e2b206e749" 1152 1166 1153 1167 [[package]] 1154 1168 name = "serde"
+1 -1
Cargo.toml
··· 7 7 anyhow = "1.0.100" 8 8 axum = "0.8.6" 9 9 dotenv = "0.15.0" 10 - minijinja = "2.12.0" 10 + minijinja = { version = "2.12.0", features = ["loader"] } 11 11 serde = "1.0.228" 12 12 sqlx = { version = "0.8.6", features = ["runtime-tokio", "sqlite"] } 13 13 tokio = { version = "1.47.1", features = ["rt-multi-thread"] }
+4 -5
src/main.rs
··· 6 6 use app_error::AppError; 7 7 mod db; 8 8 mod photos; 9 + mod templates; 9 10 use photos::Photo; 10 11 use sqlx::SqlitePool; 12 + use templates::load_templates; 11 13 use tower_http::services::ServeDir; 12 14 13 15 struct AppState { ··· 17 19 18 20 async fn root(State(state): State<Arc<AppState>>) -> Result<Html<String>, AppError> { 19 21 let photos = db::get_photos(&state.pool).await?; 20 - let template = state.template_env.get_template("index")?; 22 + let template = state.template_env.get_template("photos/index")?; 21 23 let rendered = template.render(context! { 22 24 photos => photos, 23 25 })?; ··· 32 34 .await 33 35 .expect("Where's the database???"); 34 36 35 - let mut template_env = Environment::new(); 36 - template_env.add_template("layout", include_str!("../templates/layout.jinja"))?; 37 - template_env.add_template("index", include_str!("../templates/index.jinja"))?; 38 - 37 + let template_env = load_templates()?; 39 38 let app_state = Arc::new(AppState { template_env, pool }); 40 39 let app = Router::new() 41 40 .route("/", get(root))
src/routes/lib.rs

This is a binary file and will not be displayed.

src/routes/photos.rs

This is a binary file and will not be displayed.

+50
src/templates.rs
··· 1 + use anyhow::Context; 2 + use minijinja::Environment; 3 + use std::{ 4 + fs::{self, DirEntry, read_to_string}, 5 + path::Path, 6 + }; 7 + 8 + pub fn load_templates<'a>() -> anyhow::Result<Environment<'a>> { 9 + let mut env = Environment::new(); 10 + 11 + let path = Path::new("templates"); 12 + visit_dirs(path, &mut |entry: &DirEntry| -> anyhow::Result<()> { 13 + let path = entry.path(); 14 + let file_name = path.file_stem().unwrap().to_str().unwrap().to_owned(); 15 + let parent = path.parent().unwrap(); 16 + let prefix = parent.strip_prefix("templates")?.to_str().unwrap(); 17 + let name = if prefix.is_empty() { 18 + file_name 19 + } else { 20 + format!("{prefix}/{file_name}") 21 + }; 22 + 23 + let template_str = read_to_string(entry.path()) 24 + .with_context(|| format!("read template file: {:?}", entry.path()))?; 25 + env.add_template_owned(name, template_str) 26 + .context("add_template_owned")?; 27 + 28 + Ok(()) 29 + })?; 30 + 31 + Ok(env) 32 + } 33 + 34 + fn visit_dirs( 35 + dir: &Path, 36 + cb: &mut dyn FnMut(&DirEntry) -> anyhow::Result<()>, 37 + ) -> anyhow::Result<()> { 38 + if dir.is_dir() { 39 + for entry in fs::read_dir(dir)? { 40 + let entry = entry?; 41 + let path = entry.path(); 42 + if path.is_dir() { 43 + visit_dirs(&path, cb)?; 44 + } else { 45 + cb(&entry).context("visit_dirs callback")?; 46 + } 47 + } 48 + } 49 + Ok(()) 50 + }
templates/index.jinja templates/photos/index.jinja