diff options
| author | Fuwn <[email protected]> | 2026-02-14 21:28:21 -0800 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2026-02-14 21:28:21 -0800 |
| commit | 7493510908dfc9c348c40a90e0cde96f0434b3af (patch) | |
| tree | 84f792e8b1f69dbe70f0a15012fd5f784ee9791c | |
| parent | fix(xml): Implement full XML 1.0 escaping and RSS-safe CDATA handling (diff) | |
| download | locus-7493510908dfc9c348c40a90e0cde96f0434b3af.tar.xz locus-7493510908dfc9c348c40a90e0cde96f0434b3af.zip | |
feat(blog): Treat Notion Hidden posts as private across site and RSS
| -rw-r--r-- | src/modules/blog/config.rs | 1 | ||||
| -rw-r--r-- | src/modules/blog/module.rs | 152 | ||||
| -rw-r--r-- | src/notion.rs | 31 |
3 files changed, 175 insertions, 9 deletions
diff --git a/src/modules/blog/config.rs b/src/modules/blog/config.rs index a061b7f..04c4e23 100644 --- a/src/modules/blog/config.rs +++ b/src/modules/blog/config.rs @@ -15,4 +15,5 @@ pub struct BlogPost { pub last_modified: Option<String>, pub content: String, pub blog_id: String, + pub hidden: bool, } diff --git a/src/modules/blog/module.rs b/src/modules/blog/module.rs index 51fa06b..af20488 100644 --- a/src/modules/blog/module.rs +++ b/src/modules/blog/module.rs @@ -72,10 +72,7 @@ pub fn module(router: &mut windmark::router::Router) { "This blog could not be found.", ); }; - let category_posts: Vec<_> = posts - .iter() - .filter(|post| post.blog_id == category.notion_id) - .collect(); + let category_posts = visible_posts_for_blog(&posts, &category.notion_id); let post_listing = category_posts .iter() .map(|post| { @@ -131,9 +128,8 @@ pub fn module(router: &mut windmark::router::Router) { "This blog could not be found.", ); }; - let matched_post = posts.iter().find(|post| { - post.blog_id == category.notion_id && slugify(&post.title) == post_slug - }); + let matched_post = + find_visible_post_by_slug(&posts, &category.notion_id, &post_slug); let Some(post) = matched_post else { return windmark::response::Response::not_found( "This post could not be found.", @@ -184,6 +180,26 @@ fn construct_header(post: &BlogPost) -> String { fn slugify(text: &str) -> String { text.replace(' ', "_").to_lowercase() } +fn visible_posts_for_blog<'a>( + posts: &'a [BlogPost], + blog_identifier: &str, +) -> Vec<&'a BlogPost> { + posts + .iter() + .filter(|post| post.blog_id == blog_identifier && !post.hidden) + .collect() +} + +fn find_visible_post_by_slug<'a>( + posts: &'a [BlogPost], + blog_identifier: &str, + post_slug: &str, +) -> Option<&'a BlogPost> { + visible_posts_for_blog(posts, blog_identifier) + .into_iter() + .find(|post| slugify(&post.title) == post_slug) +} + fn build_rss_feed(blog_slug: &str) -> windmark::response::Response { let categories = BLOG_CATEGORIES.read().unwrap(); let posts = BLOG_POSTS.read().unwrap(); @@ -207,7 +223,7 @@ fn build_rss_feed(blog_slug: &str) -> windmark::response::Response { xml.add_field("lastBuildDate", &chrono::Local::now().to_rfc2822()); xml.add_link(&format!("{ROOT_GEMINI_URL}/blog/{blog_slug}.xml")); - for post in posts.iter().filter(|post| post.blog_id == category.notion_id) { + for post in visible_posts_for_blog(&posts, &category.notion_id) { let post_slug = slugify(&post.title); xml.add_item(&{ @@ -314,6 +330,7 @@ fn fetch_from_notion() { }, content: page_content, blog_id: blog_identifier, + hidden: notion::extract_checkbox(&page.properties, "Hidden"), } }) .collect(); @@ -323,7 +340,7 @@ fn fetch_from_notion() { global_posts.clear(); - for post in &fetched_posts { + for post in fetched_posts.iter().filter(|post| !post.hidden) { let blog_title = categories .iter() .find(|category| category.notion_id == post.blog_id) @@ -361,3 +378,120 @@ 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, + }; + use crate::modules::blog::config::{BlogCategory, BlogPost}; + + #[test] + fn hidden_posts_are_excluded_from_rss_feed() { + let original_categories = BLOG_CATEGORIES.read().unwrap().clone(); + let original_posts = BLOG_POSTS.read().unwrap().clone(); + let category = BlogCategory { + title: "Test Blog".to_string(), + description: None, + priority: 1, + notion_id: "blog-id".to_string(), + }; + let visible_post = BlogPost { + title: "Visible".to_string(), + description: None, + author: None, + created: None, + last_modified: None, + content: "visible content".to_string(), + blog_id: "blog-id".to_string(), + hidden: false, + }; + let hidden_post = BlogPost { + title: "Hidden".to_string(), + description: None, + author: None, + created: None, + last_modified: None, + content: "hidden content".to_string(), + blog_id: "blog-id".to_string(), + hidden: true, + }; + + *BLOG_CATEGORIES.write().unwrap() = vec![category]; + *BLOG_POSTS.write().unwrap() = vec![visible_post, hidden_post]; + + let feed = build_rss_feed(&slugify("Test Blog")); + let feed_xml = feed.content; + + assert!(feed_xml.contains("<title>Visible</title>")); + assert!(!feed_xml.contains("<title>Hidden</title>")); + + *BLOG_CATEGORIES.write().unwrap() = original_categories; + *BLOG_POSTS.write().unwrap() = original_posts; + } + + #[test] + fn visible_posts_for_blog_excludes_hidden_posts() { + let visible_post = BlogPost { + title: "Visible".to_string(), + description: None, + author: None, + created: None, + last_modified: None, + content: "visible content".to_string(), + blog_id: "blog-id".to_string(), + hidden: false, + }; + let hidden_post = BlogPost { + title: "Hidden".to_string(), + description: None, + author: None, + created: None, + last_modified: None, + content: "hidden content".to_string(), + blog_id: "blog-id".to_string(), + hidden: true, + }; + + let posts = vec![visible_post, hidden_post]; + let visible_titles = visible_posts_for_blog(&posts, "blog-id") + .into_iter() + .map(|post| post.title.clone()) + .collect::<Vec<_>>(); + + assert_eq!(visible_titles, vec!["Visible".to_string()]); + } + + #[test] + fn find_visible_post_by_slug_skips_hidden_posts() { + let visible_post = BlogPost { + title: "Visible Post".to_string(), + description: None, + author: None, + created: None, + last_modified: None, + content: "visible content".to_string(), + blog_id: "blog-id".to_string(), + hidden: false, + }; + let hidden_post = BlogPost { + title: "Hidden Post".to_string(), + description: None, + author: None, + created: None, + last_modified: None, + content: "hidden content".to_string(), + blog_id: "blog-id".to_string(), + hidden: true, + }; + let posts = vec![visible_post, hidden_post]; + + assert!( + find_visible_post_by_slug(&posts, "blog-id", "visible_post").is_some() + ); + assert!( + find_visible_post_by_slug(&posts, "blog-id", "hidden_post").is_none() + ); + } +} diff --git a/src/notion.rs b/src/notion.rs index 7f9b413..006466f 100644 --- a/src/notion.rs +++ b/src/notion.rs @@ -86,6 +86,10 @@ pub fn extract_relation_ids( .unwrap_or_default() } +pub fn extract_checkbox(properties: &serde_json::Value, field: &str) -> bool { + properties[field]["checkbox"].as_bool().unwrap_or(false) +} + pub fn format_notion_date(iso_date: &str) -> String { chrono::NaiveDate::parse_from_str(iso_date, "%Y-%m-%d").map_or_else( |_| iso_date.to_string(), @@ -200,3 +204,30 @@ pub fn fetch_page_content( Ok(all_lines.join("\n")) } + +#[cfg(test)] +mod tests { + use {super::extract_checkbox, serde_json::json}; + + #[test] + fn extract_checkbox_true() { + let properties = json!({ + "Hidden": { + "checkbox": true + } + }); + + assert!(extract_checkbox(&properties, "Hidden")); + } + + #[test] + fn extract_checkbox_false_when_missing() { + let properties = json!({ + "Title": { + "title": [] + } + }); + + assert!(!extract_checkbox(&properties, "Hidden")); + } +} |