tangled
alpha
login
or
join now
ericwood.org
/
photos-site
1
fork
atom
A little app to serve my photography from my personal website
1
fork
atom
overview
issues
pulls
pipelines
bootstrap posts path
ericwood.org
2 months ago
714a3ac8
9f7410aa
+108
-36
10 changed files
expand all
collapse all
unified
split
.gitignore
src
config.rs
main.rs
post.rs
routes
mod.rs
projects
index.rs
mod.rs
show.rs
views
blog
index
mod.rs
show
mod.rs
+1
-1
.gitignore
···
1
1
/target
2
2
.env
3
3
-
/blog_posts/
4
3
.cache/
4
4
+
/content/
+18
-20
src/blog.rs
src/post.rs
···
22
22
23
23
use crate::{
24
24
AppState,
25
25
-
config::Config,
26
25
date_time::DateTime,
27
26
views::{View, blog::BlogShow},
28
27
};
29
28
30
30
-
pub struct BlogStore {
31
31
-
by_slug: HashMap<String, Arc<BlogPost>>,
32
32
-
by_tag: HashMap<String, Vec<Arc<BlogPost>>>,
29
29
+
pub struct PostStore {
30
30
+
by_slug: HashMap<String, Arc<Post>>,
31
31
+
by_tag: HashMap<String, Vec<Arc<Post>>>,
33
32
}
34
33
35
35
-
impl BlogStore {
36
36
-
pub fn new(config: &Config) -> anyhow::Result<Self> {
37
37
-
let root_path = Path::new(&config.blog_posts_path);
34
34
+
impl PostStore {
35
35
+
pub fn new(content_path: &Path, cache_path: &Path) -> anyhow::Result<Self> {
38
36
let mut by_slug = HashMap::new();
39
39
-
let mut by_tag: HashMap<String, Vec<Arc<BlogPost>>> = HashMap::new();
37
37
+
let mut by_tag: HashMap<String, Vec<Arc<Post>>> = HashMap::new();
40
38
41
41
-
let names: Vec<String> = fs::read_dir(root_path)?
39
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
56
-
let path = root_path.join(file_name).clone();
54
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
63
-
post.cache_path = Path::new(&config.cache_path).join("blog").join(&slug);
61
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
80
-
pub fn all(&self) -> Vec<Arc<BlogPost>> {
81
81
-
let mut posts: Vec<Arc<BlogPost>> = self.by_slug.values().cloned().collect();
78
78
+
pub fn all(&self) -> Vec<Arc<Post>> {
79
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
97
-
pub fn get_by_slug(&self, slug: &str) -> Option<Arc<BlogPost>> {
95
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
101
-
pub fn get_by_tag(&self, tag: &str) -> Vec<Arc<BlogPost>> {
99
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
105
-
.unwrap_or(&Vec::<Arc<BlogPost>>::new())
103
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
112
-
fn sort_posts(posts: &mut [Arc<BlogPost>]) {
110
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
121
-
pub struct BlogPost {
119
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
210
-
fn extract_frontmatter(path: &PathBuf) -> anyhow::Result<Option<BlogPost>> {
208
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
235
-
let post: BlogPost = serde_yaml::from_str(&yaml)?;
233
233
+
let post: Post = serde_yaml::from_str(&yaml)?;
236
234
237
235
Ok(Some(post))
238
236
}
+7
-3
src/config.rs
···
1
1
-
use std::env;
1
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
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
35
-
let blog_posts_path =
36
36
-
env::var("BLOG_POSTS_PATH").expect("BLOG_POSTS_PATH env variable not set");
36
36
+
let content_path = env::var("CONTENT_PATH").expect("CONTENT_PATH env variable not set");
37
37
+
let content_folder_path = Path::new(&content_path);
38
38
+
let blog_posts_path = content_folder_path.join("blog_posts").display().to_string();
39
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
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
20
-
mod blog;
20
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
30
-
use crate::blog::BlogStore;
30
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
36
-
blog_store: BlogStore,
36
36
+
blog_store: PostStore,
37
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
69
-
let blog_store = blog::BlogStore::new(&config)?;
70
70
+
let cache_path = Path::new(&config.cache_path);
71
71
+
let blog_path = Path::new(&config.blog_posts_path);
72
72
+
let blog_cache_path = cache_path.join("blog");
73
73
+
let blog_store = PostStore::new(blog_path, blog_cache_path.as_path())?;
74
74
+
75
75
+
let project_path = Path::new(&config.projects_path);
76
76
+
let project_cache_path = cache_path.join("projects");
77
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
103
-
.route("/blog/{slug}", get(routes::blog::show));
111
111
+
.route("/blog/{slug}", get(routes::blog::show))
112
112
+
.route("/projects", get(routes::projects::index))
113
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
120
+
project_store,
110
121
});
111
122
112
112
-
blog::cache_posts(&app_state)?;
123
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
3
+
pub mod projects;
+29
src/routes/projects/index.rs
···
1
1
+
use crate::{
2
2
+
AppState, Response,
3
3
+
views::{View, blog::BlogIndex},
4
4
+
};
5
5
+
use axum::{
6
6
+
extract::{Query, State},
7
7
+
response::Html,
8
8
+
};
9
9
+
use serde::Deserialize;
10
10
+
use std::sync::Arc;
11
11
+
12
12
+
#[derive(Deserialize)]
13
13
+
pub struct IndexParams {
14
14
+
pub tag: Option<String>,
15
15
+
}
16
16
+
17
17
+
pub async fn index(query: Query<IndexParams>, State(state): State<Arc<AppState>>) -> Response {
18
18
+
let posts = if let Some(tag) = query.tag.clone() {
19
19
+
state.project_store.get_by_tag(&tag)
20
20
+
} else {
21
21
+
state.project_store.all()
22
22
+
};
23
23
+
24
24
+
let tags = state.project_store.all_tags();
25
25
+
let view = BlogIndex::new(posts, tags, query.tag.clone());
26
26
+
let html = view.render(&state.reloader)?;
27
27
+
28
28
+
Ok(Html(html))
29
29
+
}
+5
src/routes/projects/mod.rs
···
1
1
+
mod index;
2
2
+
mod show;
3
3
+
4
4
+
pub use index::index;
5
5
+
pub use show::show;
+24
src/routes/projects/show.rs
···
1
1
+
use crate::views::{View, blog::BlogShow};
2
2
+
use crate::{AppState, Response, app_error::AppError};
3
3
+
use axum::{
4
4
+
extract::{Path, State},
5
5
+
response::Html,
6
6
+
};
7
7
+
use std::{fs::read_to_string, sync::Arc};
8
8
+
9
9
+
pub async fn show(Path(slug): Path<String>, State(state): State<Arc<AppState>>) -> Response {
10
10
+
let post = state
11
11
+
.project_store
12
12
+
.get_by_slug(&slug)
13
13
+
.ok_or(AppError::NotFound)?;
14
14
+
15
15
+
if state.config.is_prod() {
16
16
+
let html = read_to_string(&post.cache_path)?;
17
17
+
return Ok(Html(html));
18
18
+
}
19
19
+
20
20
+
let view = BlogShow::new(post);
21
21
+
let html = view.render(&state.reloader)?;
22
22
+
23
23
+
Ok(Html(html))
24
24
+
}
+3
-3
src/views/blog/index/mod.rs
···
3
3
use minijinja::context;
4
4
use minijinja_autoreload::AutoReloader;
5
5
6
6
-
use crate::{blog::BlogPost, templates::render, views::View};
6
6
+
use crate::{post::Post, templates::render, views::View};
7
7
8
8
pub struct BlogIndex {
9
9
-
posts: Vec<Arc<BlogPost>>,
9
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
15
-
pub fn new(posts: Vec<Arc<BlogPost>>, tags: Vec<(String, usize)>, tag: Option<String>) -> Self {
15
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
7
-
blog::{BlogPost, Section, render_post},
7
7
+
post::{Post, Section, render_post},
8
8
templates::render,
9
9
views::View,
10
10
};
11
11
12
12
pub struct BlogShow {
13
13
-
post: Arc<BlogPost>,
13
13
+
post: Arc<Post>,
14
14
}
15
15
16
16
impl BlogShow {
17
17
-
pub fn new(post: Arc<BlogPost>) -> Self {
17
17
+
pub fn new(post: Arc<Post>) -> Self {
18
18
Self { post }
19
19
}
20
20
}