A little app to serve my photography from my personal website

blog skeleton

+122 -21
+1
.gitignore
··· 1 1 /target 2 2 .env 3 + /blog_posts/
assets/.DS_Store

This is a binary file and will not be displayed.

+31
src/blog.rs
··· 1 + use std::{ 2 + collections::HashMap, 3 + env, fs, 4 + path::{Path, PathBuf}, 5 + }; 6 + 7 + pub fn get_posts() -> anyhow::Result<HashMap<String, PathBuf>> { 8 + let path_str = env::var("BLOG_POSTS_PATH").expect("BLOG_POSTS_PATH not set"); 9 + let root_path = Path::new(&path_str); 10 + let mut map = HashMap::new(); 11 + 12 + let names: Vec<String> = fs::read_dir(root_path)? 13 + .filter_map(|i| i.ok()) 14 + .filter_map(|entry| { 15 + let file_name = entry.file_name().into_string().unwrap_or("".to_string()); 16 + if !file_name.ends_with("md") { 17 + return None; 18 + } 19 + 20 + Some(file_name) 21 + }) 22 + .collect(); 23 + 24 + for file_name in names { 25 + let mut slug = file_name.replace("_", "-"); 26 + slug.replace_last(".md", ""); 27 + let path = root_path.join(file_name).clone(); 28 + map.insert(slug, path); 29 + } 30 + Ok(map) 31 + }
+17 -4
src/main.rs
··· 1 + #![feature(string_replace_in_place)] 1 2 use axum::{ 2 3 Router, 3 4 http::{HeaderValue, header}, ··· 6 7 }; 7 8 use dotenvy::dotenv; 8 9 use minijinja_autoreload::AutoReloader; 9 - use std::{env, sync::Arc}; 10 + use std::{collections::HashMap, env, path::PathBuf, sync::Arc}; 10 11 use tower::ServiceBuilder; 11 12 mod app_error; 12 13 use app_error::AppError; ··· 16 17 mod routes; 17 18 mod templates; 18 19 use axum_tracing_opentelemetry::middleware::{OtelAxumLayer, OtelInResponseLayer}; 20 + mod blog; 19 21 use init_tracing_opentelemetry::TracingConfig; 20 22 use models::{Photo, Tag}; 21 23 use sqlx::SqlitePool; ··· 24 26 25 27 struct AppState { 26 28 reloader: AutoReloader, 27 - pool: SqlitePool, 29 + photos_db_pool: SqlitePool, 30 + blog_slugs: HashMap<String, PathBuf>, 28 31 } 32 + 33 + type Response = Result<Html<String>, AppError>; 29 34 30 35 #[tokio::main] 31 36 async fn main() -> anyhow::Result<()> { ··· 39 44 40 45 let _guard = tracing_config.init_subscriber()?; 41 46 42 - let pool = SqlitePool::connect(&env::var("DATABASE_URL")?) 47 + let photos_db_pool = SqlitePool::connect(&env::var("DATABASE_URL")?) 43 48 .await 44 49 .expect("Where's the database???"); 45 50 46 51 tracing::info!("connected to DB"); 47 52 53 + let blog_slugs = blog::get_posts()?; 54 + 48 55 let reloader = load_templates_dyn(); 49 - let app_state = Arc::new(AppState { reloader, pool }); 56 + let app_state = Arc::new(AppState { 57 + reloader, 58 + photos_db_pool, 59 + blog_slugs, 60 + }); 50 61 let app = Router::new() 51 62 .nest_service( 52 63 "/photos/assets", ··· 77 88 ) 78 89 .route("/photos", get(routes::photos::index)) 79 90 .route("/photos/{id}", get(routes::photos::show)) 91 + .route("/blog", get(routes::blog::index)) 92 + .route("/blog/{slug}", get(routes::blog::show)) 80 93 .with_state(app_state) 81 94 .fallback(handler_404) 82 95 .layer(OtelInResponseLayer)
+17
src/routes/blog/index.rs
··· 1 + use crate::{AppState, Response, templates::render}; 2 + use axum::{extract::State, response::Html}; 3 + use minijinja::context; 4 + use std::sync::Arc; 5 + 6 + pub async fn index(State(state): State<Arc<AppState>>) -> Response { 7 + let slugs: Vec<&String> = state.blog_slugs.keys().collect(); 8 + let rendered = render( 9 + &state.reloader, 10 + "blog/index", 11 + context! { 12 + slugs 13 + }, 14 + )?; 15 + 16 + Ok(Html(rendered)) 17 + }
+5
src/routes/blog/mod.rs
··· 1 + mod index; 2 + mod show; 3 + 4 + pub use index::index; 5 + pub use show::show;
+19
src/routes/blog/show.rs
··· 1 + use crate::{AppState, Response, templates::render}; 2 + use axum::{ 3 + extract::{Path, State}, 4 + response::Html, 5 + }; 6 + use minijinja::context; 7 + use std::sync::Arc; 8 + 9 + pub async fn show(Path(slug): Path<String>, State(state): State<Arc<AppState>>) -> Response { 10 + let rendered = render( 11 + &state.reloader, 12 + "blog/show", 13 + context! { 14 + slug 15 + }, 16 + )?; 17 + 18 + Ok(Html(rendered)) 19 + }
+1
src/routes/mod.rs
··· 1 + pub mod blog; 1 2 pub mod photos;
+4 -7
src/routes/photos/index.rs
··· 7 7 use serde_valid::Validate; 8 8 9 9 use crate::{ 10 - AppError, AppState, 10 + AppState, Response, 11 11 db::{self, Pagination as QueryPagination, PhotoQuery, Sort, SortDirection, SortField}, 12 12 models::Tag, 13 13 templates::render, ··· 61 61 62 62 static SORT_FIELDS: [SortField; 2] = [SortField::TakenAt, SortField::CreatedAt]; 63 63 64 - pub async fn index( 65 - query: Query<IndexParams>, 66 - State(state): State<Arc<AppState>>, 67 - ) -> Result<Html<String>, AppError> { 64 + pub async fn index(query: Query<IndexParams>, State(state): State<Arc<AppState>>) -> Response { 68 65 query.validate()?; 69 66 let query = query.0; 70 67 let default = IndexParams::default(); ··· 75 72 let dir = query.dir.unwrap_or(default.dir.unwrap()); 76 73 77 74 let (total_photos, photos) = db::get_photos( 78 - &state.pool, 75 + &state.photos_db_pool, 79 76 PhotoQuery { 80 77 sort: Sort { 81 78 field: sort, ··· 94 91 let num_pages = (total_photos as f32 / limit as f32).ceil() as u32; 95 92 let pagination = get_pagination(&query, num_pages)?; 96 93 97 - let all_tags = db::get_tags(&state.pool, &query.tags).await?; 94 + let all_tags = db::get_tags(&state.photos_db_pool, &query.tags).await?; 98 95 let (current_tags, tags) = process_tags(&all_tags, &query)?; 99 96 100 97 let rendered = render(
+9 -10
src/routes/photos/show.rs
··· 1 - use crate::{AppError, AppState, db, templates::render}; 1 + use crate::{AppError, AppState, Response, db, templates::render}; 2 2 use axum::{ 3 3 extract::{Path, State}, 4 4 response::Html, ··· 6 6 use minijinja::context; 7 7 use std::sync::Arc; 8 8 9 - pub async fn show( 10 - Path(id): Path<String>, 11 - State(state): State<Arc<AppState>>, 12 - ) -> Result<Html<String>, AppError> { 13 - let photo = db::get_photo(&state.pool, &id).await.map_err(|e| match e { 14 - sqlx::Error::RowNotFound => AppError::NotFound, 15 - _ => AppError::DbError(e), 16 - })?; 9 + pub async fn show(Path(id): Path<String>, State(state): State<Arc<AppState>>) -> Response { 10 + let photo = db::get_photo(&state.photos_db_pool, &id) 11 + .await 12 + .map_err(|e| match e { 13 + sqlx::Error::RowNotFound => AppError::NotFound, 14 + _ => AppError::DbError(e), 15 + })?; 17 16 18 - let tags = db::get_photo_tags(&state.pool, &id).await?; 17 + let tags = db::get_photo_tags(&state.photos_db_pool, &id).await?; 19 18 let aperture = format!("{:.1}", photo.aperture); 20 19 let focal_length = format!("{:.0}", photo.focal_length); 21 20 let shutter_speed = format!("1/{:.0}s", 1.0 / photo.shutter_speed);
+11
templates/blog/index.jinja
··· 1 + {% extends "layout" %} 2 + {% set active_page = 'blog' %} 3 + {% block title %}Blog{% endblock %} 4 + 5 + {% block body %} 6 + <ul> 7 + {% for slug in slugs %} 8 + <li><a href="/blog/{{ slug }}">{{ slug }}</a></li> 9 + {% endfor %} 10 + </ul> 11 + {% endblock %}
+7
templates/blog/show.jinja
··· 1 + {% extends "layout" %} 2 + {% set active_page = 'blog' %} 3 + {% block title %}Blog{% endblock %} 4 + 5 + {% block body %} 6 + {{ slug }} 7 + {% endblock %}