aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFuwn <[email protected]>2026-02-14 21:40:04 -0800
committerFuwn <[email protected]>2026-02-14 21:40:04 -0800
commitb3e7d5821878c43c16b3597b19f2149845259630 (patch)
tree31b95c9315c24ebb9673c60a473a0150d2b5705b
parentfeat(blog): Treat Notion Hidden posts as private across site and RSS (diff)
downloadlocus-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.rs1
-rw-r--r--src/modules/blog/module.rs197
-rw-r--r--src/modules/blog/post.rs11
-rw-r--r--src/modules/index.rs6
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()))