diff options
| author | Fuwn <[email protected]> | 2026-02-14 21:40:04 -0800 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2026-02-14 21:40:04 -0800 |
| commit | b3e7d5821878c43c16b3597b19f2149845259630 (patch) | |
| tree | 31b95c9315c24ebb9673c60a473a0150d2b5705b | |
| parent | feat(blog): Treat Notion Hidden posts as private across site and RSS (diff) | |
| download | locus-b3e7d5821878c43c16b3597b19f2149845259630.tar.xz locus-b3e7d5821878c43c16b3597b19f2149845259630.zip | |
feat(blog): Sort blog listings and recent posts by true chronological date
| -rw-r--r-- | src/modules/blog/config.rs | 1 | ||||
| -rw-r--r-- | src/modules/blog/module.rs | 197 | ||||
| -rw-r--r-- | src/modules/blog/post.rs | 11 | ||||
| -rw-r--r-- | src/modules/index.rs | 6 |
4 files changed, 183 insertions, 32 deletions
diff --git a/src/modules/blog/config.rs b/src/modules/blog/config.rs index 04c4e23..37dbb9d 100644 --- a/src/modules/blog/config.rs +++ b/src/modules/blog/config.rs @@ -11,6 +11,7 @@ pub struct BlogPost { pub title: String, pub description: Option<String>, pub author: Option<String>, + pub created_raw: Option<String>, pub created: Option<String>, pub last_modified: Option<String>, pub content: String, diff --git a/src/modules/blog/module.rs b/src/modules/blog/module.rs index af20488..e16861a 100644 --- a/src/modules/blog/module.rs +++ b/src/modules/blog/module.rs @@ -180,14 +180,42 @@ fn construct_header(post: &BlogPost) -> String { fn slugify(text: &str) -> String { text.replace(' ', "_").to_lowercase() } +fn parse_chronological_sort_key(raw_date: Option<&str>) -> i64 { + raw_date + .and_then(|date_text| { + chrono::DateTime::parse_from_rfc3339(date_text) + .ok() + .map(|date_time| date_time.timestamp()) + .or_else(|| { + chrono::NaiveDate::parse_from_str(date_text, "%Y-%m-%d") + .ok() + .and_then(|date| date.and_hms_opt(0, 0, 0)) + .map(|date_time| date_time.and_utc().timestamp()) + }) + }) + .unwrap_or(i64::MIN) +} + +fn sort_posts_by_created_desc(posts: &mut Vec<&BlogPost>) { + posts.sort_by(|first_post, second_post| { + parse_chronological_sort_key(second_post.created_raw.as_deref()) + .cmp(&parse_chronological_sort_key(first_post.created_raw.as_deref())) + .then_with(|| first_post.title.cmp(&second_post.title)) + }); +} + fn visible_posts_for_blog<'a>( posts: &'a [BlogPost], blog_identifier: &str, ) -> Vec<&'a BlogPost> { - posts + let mut visible_posts: Vec<_> = posts .iter() .filter(|post| post.blog_id == blog_identifier && !post.hidden) - .collect() + .collect(); + + sort_posts_by_created_desc(&mut visible_posts); + + visible_posts } fn find_visible_post_by_slug<'a>( @@ -253,6 +281,32 @@ fn build_rss_feed(blog_slug: &str) -> windmark::response::Response { .clone() } +fn build_global_posts( + categories: &[BlogCategory], + fetched_posts: &[BlogPost], +) -> Vec<Post> { + let mut visible_posts: Vec<_> = + fetched_posts.iter().filter(|post| !post.hidden).collect(); + + sort_posts_by_created_desc(&mut visible_posts); + + visible_posts + .into_iter() + .map(|post| { + let blog_title = categories + .iter() + .find(|category| category.notion_id == post.blog_id) + .map(|category| category.title.clone()) + .unwrap_or_default(); + + Post::new( + format!("{}, {}", blog_title, post.title), + format!("/blog/{}/{}", slugify(&blog_title), slugify(&post.title)), + ) + }) + .collect() +} + fn fetch_from_notion() { let api_key = std::env::var("NOTION_API_KEY") .expect("NOTION_API_KEY environment variable is required"); @@ -318,6 +372,11 @@ fn fetch_from_notion() { } else { Some(author_text) }, + created_raw: if created_raw.is_empty() { + None + } else { + Some(created_raw.clone()) + }, created: if created_raw.is_empty() { None } else { @@ -338,21 +397,7 @@ fn fetch_from_notion() { { let mut global_posts = POSTS.lock().unwrap(); - global_posts.clear(); - - for post in fetched_posts.iter().filter(|post| !post.hidden) { - let blog_title = categories - .iter() - .find(|category| category.notion_id == post.blog_id) - .map(|category| category.title.clone()) - .unwrap_or_default(); - - global_posts.push(Post::new( - format!("{}, {}", blog_title, post.title), - format!("/blog/{}/{}", slugify(&blog_title), slugify(&post.title)), - post.created.clone().unwrap_or_default(), - )); - } + *global_posts = build_global_posts(&categories, &fetched_posts); } *BLOG_CATEGORIES.write().unwrap() = categories; @@ -382,8 +427,8 @@ pub fn refresh_loop() { #[cfg(test)] mod tests { use super::{ - build_rss_feed, find_visible_post_by_slug, slugify, visible_posts_for_blog, - BLOG_CATEGORIES, BLOG_POSTS, + build_global_posts, build_rss_feed, find_visible_post_by_slug, slugify, + visible_posts_for_blog, BLOG_CATEGORIES, BLOG_POSTS, }; use crate::modules::blog::config::{BlogCategory, BlogPost}; @@ -401,6 +446,7 @@ mod tests { title: "Visible".to_string(), description: None, author: None, + created_raw: None, created: None, last_modified: None, content: "visible content".to_string(), @@ -411,6 +457,7 @@ mod tests { title: "Hidden".to_string(), description: None, author: None, + created_raw: None, created: None, last_modified: None, content: "hidden content".to_string(), @@ -437,6 +484,7 @@ mod tests { title: "Visible".to_string(), description: None, author: None, + created_raw: None, created: None, last_modified: None, content: "visible content".to_string(), @@ -447,6 +495,7 @@ mod tests { title: "Hidden".to_string(), description: None, author: None, + created_raw: None, created: None, last_modified: None, content: "hidden content".to_string(), @@ -469,6 +518,7 @@ mod tests { title: "Visible Post".to_string(), description: None, author: None, + created_raw: None, created: None, last_modified: None, content: "visible content".to_string(), @@ -479,6 +529,7 @@ mod tests { title: "Hidden Post".to_string(), description: None, author: None, + created_raw: None, created: None, last_modified: None, content: "hidden content".to_string(), @@ -494,4 +545,112 @@ mod tests { find_visible_post_by_slug(&posts, "blog-id", "hidden_post").is_none() ); } + + #[test] + fn visible_posts_for_blog_are_sorted_chronologically_desc() { + let oldest_post = BlogPost { + title: "Oldest".to_string(), + description: None, + author: None, + created_raw: Some("2024-01-01".to_string()), + created: Some("January 1, 2024".to_string()), + last_modified: None, + content: "oldest content".to_string(), + blog_id: "blog-id".to_string(), + hidden: false, + }; + let newest_post = BlogPost { + title: "Newest".to_string(), + description: None, + author: None, + created_raw: Some("2026-03-10".to_string()), + created: Some("March 10, 2026".to_string()), + last_modified: None, + content: "newest content".to_string(), + blog_id: "blog-id".to_string(), + hidden: false, + }; + let middle_post = BlogPost { + title: "Middle".to_string(), + description: None, + author: None, + created_raw: Some("2025-02-02".to_string()), + created: Some("February 2, 2025".to_string()), + last_modified: None, + content: "middle content".to_string(), + blog_id: "blog-id".to_string(), + hidden: false, + }; + let posts = vec![oldest_post, newest_post, middle_post]; + let ordered_titles = visible_posts_for_blog(&posts, "blog-id") + .iter() + .map(|post| post.title.clone()) + .collect::<Vec<_>>(); + + assert_eq!( + ordered_titles, + vec![ + "Newest".to_string(), + "Middle".to_string(), + "Oldest".to_string() + ] + ); + } + + #[test] + fn global_posts_are_sorted_chronologically_desc() { + let categories = vec![BlogCategory { + title: "Test Blog".to_string(), + description: None, + priority: 1, + notion_id: "blog-id".to_string(), + }]; + let older_post = BlogPost { + title: "Older".to_string(), + description: None, + author: None, + created_raw: Some("2025-01-01".to_string()), + created: Some("January 1, 2025".to_string()), + last_modified: None, + content: "older content".to_string(), + blog_id: "blog-id".to_string(), + hidden: false, + }; + let newer_post = BlogPost { + title: "Newer".to_string(), + description: None, + author: None, + created_raw: Some("2026-01-01".to_string()), + created: Some("January 1, 2026".to_string()), + last_modified: None, + content: "newer content".to_string(), + blog_id: "blog-id".to_string(), + hidden: false, + }; + let hidden_newest_post = BlogPost { + title: "Hidden".to_string(), + description: None, + author: None, + created_raw: Some("2027-01-01".to_string()), + created: Some("January 1, 2027".to_string()), + last_modified: None, + content: "hidden content".to_string(), + blog_id: "blog-id".to_string(), + hidden: true, + }; + let posts = vec![older_post, hidden_newest_post, newer_post]; + let global_posts = build_global_posts(&categories, &posts); + let ordered_titles = global_posts + .iter() + .map(|post| post.title().clone()) + .collect::<Vec<_>>(); + + assert_eq!( + ordered_titles, + vec![ + "Test Blog, Newer".to_string(), + "Test Blog, Older".to_string() + ] + ); + } } diff --git a/src/modules/blog/post.rs b/src/modules/blog/post.rs index fb697ae..3422b85 100644 --- a/src/modules/blog/post.rs +++ b/src/modules/blog/post.rs @@ -1,17 +1,12 @@ pub struct Post { - title: String, - link: String, - created_at: String, + title: String, + link: String, } impl Post { - pub const fn new(title: String, link: String, created_at: String) -> Self { - Self { title, link, created_at } - } + pub const fn new(title: String, link: String) -> Self { Self { title, link } } pub const fn title(&self) -> &String { &self.title } pub const fn link(&self) -> &String { &self.link } - - pub const fn created_at(&self) -> &String { &self.created_at } } diff --git a/src/modules/index.rs b/src/modules/index.rs index 1dfa66f..363a14f 100644 --- a/src/modules/index.rs +++ b/src/modules/index.rs @@ -35,11 +35,7 @@ Don't know where to start? Check out The Directory or test your luck! (*POSTS).lock().map_or_else( |_| "...".to_string(), |global_posts| { - let mut posts = global_posts; - - posts.sort_by(|a, b| b.created_at().cmp(a.created_at())); - - posts + global_posts .iter() .take(3) .map(|post| format!("=> {} {}", post.link(), post.title())) |