A little app to serve my photography from my personal website

bootstrap posts path

+108 -36
+1 -1
.gitignore
··· 1 1 /target 2 2 .env 3 - /blog_posts/ 4 3 .cache/ 4 + /content/
+18 -20
src/blog.rs src/post.rs
··· 22 22 23 23 use crate::{ 24 24 AppState, 25 - config::Config, 26 25 date_time::DateTime, 27 26 views::{View, blog::BlogShow}, 28 27 }; 29 28 30 - pub struct BlogStore { 31 - by_slug: HashMap<String, Arc<BlogPost>>, 32 - by_tag: HashMap<String, Vec<Arc<BlogPost>>>, 29 + pub struct PostStore { 30 + by_slug: HashMap<String, Arc<Post>>, 31 + by_tag: HashMap<String, Vec<Arc<Post>>>, 33 32 } 34 33 35 - impl BlogStore { 36 - pub fn new(config: &Config) -> anyhow::Result<Self> { 37 - let root_path = Path::new(&config.blog_posts_path); 34 + impl PostStore { 35 + pub fn new(content_path: &Path, cache_path: &Path) -> anyhow::Result<Self> { 38 36 let mut by_slug = HashMap::new(); 39 - let mut by_tag: HashMap<String, Vec<Arc<BlogPost>>> = HashMap::new(); 37 + let mut by_tag: HashMap<String, Vec<Arc<Post>>> = HashMap::new(); 40 38 41 - let names: Vec<String> = fs::read_dir(root_path)? 39 + let names: Vec<String> = fs::read_dir(content_path)? 42 40 .filter_map(|i| i.ok()) 43 41 .filter_map(|entry| { 44 42 let file_name = entry.file_name().into_string().unwrap_or("".to_string()); ··· 53 51 for file_name in names { 54 52 let mut slug = slug::slugify(&file_name); 55 53 slug.replace_last("-md", ""); 56 - let path = root_path.join(file_name).clone(); 54 + let path = content_path.join(file_name).clone(); 57 55 58 56 let maybe_post = extract_frontmatter(&path)?; 59 57 60 58 if let Some(mut post) = maybe_post { 61 59 post.slug = Some(slug.clone()); 62 60 post.file_path = path; 63 - post.cache_path = Path::new(&config.cache_path).join("blog").join(&slug); 61 + post.cache_path = cache_path.join(&slug); 64 62 let post = Arc::new(post); 65 63 by_slug.insert(slug, post.clone()); 66 64 ··· 77 75 Ok(Self { by_slug, by_tag }) 78 76 } 79 77 80 - pub fn all(&self) -> Vec<Arc<BlogPost>> { 81 - let mut posts: Vec<Arc<BlogPost>> = self.by_slug.values().cloned().collect(); 78 + pub fn all(&self) -> Vec<Arc<Post>> { 79 + let mut posts: Vec<Arc<Post>> = self.by_slug.values().cloned().collect(); 82 80 sort_posts(&mut posts); 83 81 posts 84 82 } ··· 94 92 tags 95 93 } 96 94 97 - pub fn get_by_slug(&self, slug: &str) -> Option<Arc<BlogPost>> { 95 + pub fn get_by_slug(&self, slug: &str) -> Option<Arc<Post>> { 98 96 self.by_slug.get(slug).cloned() 99 97 } 100 98 101 - pub fn get_by_tag(&self, tag: &str) -> Vec<Arc<BlogPost>> { 99 + pub fn get_by_tag(&self, tag: &str) -> Vec<Arc<Post>> { 102 100 let mut posts = self 103 101 .by_tag 104 102 .get(tag) 105 - .unwrap_or(&Vec::<Arc<BlogPost>>::new()) 103 + .unwrap_or(&Vec::<Arc<Post>>::new()) 106 104 .to_vec(); 107 105 sort_posts(&mut posts); 108 106 posts 109 107 } 110 108 } 111 109 112 - fn sort_posts(posts: &mut [Arc<BlogPost>]) { 110 + fn sort_posts(posts: &mut [Arc<Post>]) { 113 111 posts.sort_by(|a, b| { 114 112 let a = a.published_at.clone().unwrap_or(DateTime::min_date()); 115 113 let b = b.published_at.clone().unwrap_or(DateTime::min_date()); ··· 118 116 } 119 117 120 118 #[derive(serde::Deserialize, serde::Serialize, Clone)] 121 - pub struct BlogPost { 119 + pub struct Post { 122 120 pub title: Option<String>, 123 121 pub published_at: Option<DateTime>, 124 122 pub slug: Option<String>, ··· 207 205 } 208 206 } 209 207 210 - fn extract_frontmatter(path: &PathBuf) -> anyhow::Result<Option<BlogPost>> { 208 + fn extract_frontmatter(path: &PathBuf) -> anyhow::Result<Option<Post>> { 211 209 let file = File::open(path)?; 212 210 213 211 let mut yaml = String::new(); ··· 232 230 yaml.push_str(&format!("\n{line}")); 233 231 } 234 232 235 - let post: BlogPost = serde_yaml::from_str(&yaml)?; 233 + let post: Post = serde_yaml::from_str(&yaml)?; 236 234 237 235 Ok(Some(post)) 238 236 }
+7 -3
src/config.rs
··· 1 - use std::env; 1 + use std::{env, path::Path}; 2 2 3 3 #[derive(PartialEq)] 4 4 pub enum Environment { ··· 11 11 pub cache_path: String, 12 12 pub auto_reload_templates: bool, 13 13 pub blog_posts_path: String, 14 + pub projects_path: String, 14 15 pub photos_db_path: String, 15 16 pub photos_thumbnail_path: String, 16 17 pub photos_image_path: String, ··· 32 33 33 34 let auto_reload_templates = env::var("AUTO_RELOAD_TEMPLATES").is_ok_and(|c| c == "true"); 34 35 35 - let blog_posts_path = 36 - env::var("BLOG_POSTS_PATH").expect("BLOG_POSTS_PATH env variable not set"); 36 + let content_path = env::var("CONTENT_PATH").expect("CONTENT_PATH env variable not set"); 37 + let content_folder_path = Path::new(&content_path); 38 + let blog_posts_path = content_folder_path.join("blog_posts").display().to_string(); 39 + let projects_path = content_folder_path.join("projects").display().to_string(); 37 40 38 41 let photos_db_path = 39 42 env::var("PHOTOS_DB_PATH").expect("PHOTOS_DB_PATH env variable not set"); ··· 48 51 cache_path, 49 52 auto_reload_templates, 50 53 blog_posts_path, 54 + projects_path, 51 55 photos_db_path, 52 56 photos_thumbnail_path, 53 57 photos_image_path,
+17 -6
src/main.rs
··· 17 17 mod routes; 18 18 mod templates; 19 19 use axum_tracing_opentelemetry::middleware::{OtelAxumLayer, OtelInResponseLayer}; 20 - mod blog; 20 + mod post; 21 21 use init_tracing_opentelemetry::TracingConfig; 22 22 use models::{Photo, Tag}; 23 23 use sqlx::SqlitePool; ··· 27 27 use config::Config; 28 28 mod views; 29 29 30 - use crate::blog::BlogStore; 30 + use post::PostStore; 31 31 32 32 struct AppState { 33 33 config: Config, 34 34 reloader: AutoReloader, 35 35 photos_db_pool: SqlitePool, 36 - blog_store: BlogStore, 36 + blog_store: PostStore, 37 + project_store: PostStore, 37 38 } 38 39 39 40 type Response = Result<Html<String>, AppError>; ··· 66 67 67 68 tracing::info!("connected to DB"); 68 69 69 - let blog_store = blog::BlogStore::new(&config)?; 70 + let cache_path = Path::new(&config.cache_path); 71 + let blog_path = Path::new(&config.blog_posts_path); 72 + let blog_cache_path = cache_path.join("blog"); 73 + let blog_store = PostStore::new(blog_path, blog_cache_path.as_path())?; 74 + 75 + let project_path = Path::new(&config.projects_path); 76 + let project_cache_path = cache_path.join("projects"); 77 + let project_store = PostStore::new(project_path, project_cache_path.as_path())?; 70 78 71 79 let reloader = load_templates_dyn(&config); 72 80 let app = Router::new() ··· 100 108 .route("/photos", get(routes::photos::index)) 101 109 .route("/photos/{id}", get(routes::photos::show)) 102 110 .route("/blog", get(routes::blog::index)) 103 - .route("/blog/{slug}", get(routes::blog::show)); 111 + .route("/blog/{slug}", get(routes::blog::show)) 112 + .route("/projects", get(routes::projects::index)) 113 + .route("/projects/{slug}", get(routes::projects::show)); 104 114 105 115 let app_state = Arc::new(AppState { 106 116 config, 107 117 reloader, 108 118 photos_db_pool, 109 119 blog_store, 120 + project_store, 110 121 }); 111 122 112 - blog::cache_posts(&app_state)?; 123 + post::cache_posts(&app_state)?; 113 124 114 125 let app = app 115 126 .with_state(app_state)
+1
src/routes/mod.rs
··· 1 1 pub mod blog; 2 2 pub mod photos; 3 + pub mod projects;
+29
src/routes/projects/index.rs
··· 1 + use crate::{ 2 + AppState, Response, 3 + views::{View, blog::BlogIndex}, 4 + }; 5 + use axum::{ 6 + extract::{Query, State}, 7 + response::Html, 8 + }; 9 + use serde::Deserialize; 10 + use std::sync::Arc; 11 + 12 + #[derive(Deserialize)] 13 + pub struct IndexParams { 14 + pub tag: Option<String>, 15 + } 16 + 17 + pub async fn index(query: Query<IndexParams>, State(state): State<Arc<AppState>>) -> Response { 18 + let posts = if let Some(tag) = query.tag.clone() { 19 + state.project_store.get_by_tag(&tag) 20 + } else { 21 + state.project_store.all() 22 + }; 23 + 24 + let tags = state.project_store.all_tags(); 25 + let view = BlogIndex::new(posts, tags, query.tag.clone()); 26 + let html = view.render(&state.reloader)?; 27 + 28 + Ok(Html(html)) 29 + }
+5
src/routes/projects/mod.rs
··· 1 + mod index; 2 + mod show; 3 + 4 + pub use index::index; 5 + pub use show::show;
+24
src/routes/projects/show.rs
··· 1 + use crate::views::{View, blog::BlogShow}; 2 + use crate::{AppState, Response, app_error::AppError}; 3 + use axum::{ 4 + extract::{Path, State}, 5 + response::Html, 6 + }; 7 + use std::{fs::read_to_string, sync::Arc}; 8 + 9 + pub async fn show(Path(slug): Path<String>, State(state): State<Arc<AppState>>) -> Response { 10 + let post = state 11 + .project_store 12 + .get_by_slug(&slug) 13 + .ok_or(AppError::NotFound)?; 14 + 15 + if state.config.is_prod() { 16 + let html = read_to_string(&post.cache_path)?; 17 + return Ok(Html(html)); 18 + } 19 + 20 + let view = BlogShow::new(post); 21 + let html = view.render(&state.reloader)?; 22 + 23 + Ok(Html(html)) 24 + }
+3 -3
src/views/blog/index/mod.rs
··· 3 3 use minijinja::context; 4 4 use minijinja_autoreload::AutoReloader; 5 5 6 - use crate::{blog::BlogPost, templates::render, views::View}; 6 + use crate::{post::Post, templates::render, views::View}; 7 7 8 8 pub struct BlogIndex { 9 - posts: Vec<Arc<BlogPost>>, 9 + posts: Vec<Arc<Post>>, 10 10 tags: Vec<(String, usize)>, 11 11 tag: Option<String>, 12 12 } 13 13 14 14 impl BlogIndex { 15 - pub fn new(posts: Vec<Arc<BlogPost>>, tags: Vec<(String, usize)>, tag: Option<String>) -> Self { 15 + pub fn new(posts: Vec<Arc<Post>>, tags: Vec<(String, usize)>, tag: Option<String>) -> Self { 16 16 Self { posts, tags, tag } 17 17 } 18 18 }
+3 -3
src/views/blog/show/mod.rs
··· 4 4 use minijinja_autoreload::AutoReloader; 5 5 6 6 use crate::{ 7 - blog::{BlogPost, Section, render_post}, 7 + post::{Post, Section, render_post}, 8 8 templates::render, 9 9 views::View, 10 10 }; 11 11 12 12 pub struct BlogShow { 13 - post: Arc<BlogPost>, 13 + post: Arc<Post>, 14 14 } 15 15 16 16 impl BlogShow { 17 - pub fn new(post: Arc<BlogPost>) -> Self { 17 + pub fn new(post: Arc<Post>) -> Self { 18 18 Self { post } 19 19 } 20 20 }