//! Atom feed generation. use crate::config::SiteConfig; use crate::content::Content; use std::path::Path; /// Generate an Atom 1.0 feed from blog posts. pub fn generate_atom_feed(posts: &[Content], config: &SiteConfig, content_root: &Path) -> String { let base_url = config.base_url.trim_end_matches('/'); // Use the most recent post date as feed updated time, or fallback let updated = posts .first() .and_then(|p| p.frontmatter.date.as_ref()) .map(|d| format!("{}T00:00:00Z", d)) .unwrap_or_else(|| "1970-01-01T00:00:00Z".to_string()); let mut entries = String::new(); for post in posts { // Derive URL from output path (e.g., blog/foo.html → /blog/foo.html) let relative_path = post.output_path(content_root); let post_url = format!("{}/{}", base_url, relative_path.display()); let post_date = post .frontmatter .date .as_ref() .map(|d| format!("{}T00:00:00Z", d)) .unwrap_or_else(|| "1970-01-01T00:00:00Z".to_string()); let summary = post .frontmatter .description .as_ref() .map(|s| xml_escape(s)) .unwrap_or_default(); entries.push_str(&format!( r#" {} {} {} {} "#, xml_escape(&post.frontmatter.title), post_url, post_url, post_date, summary, )); } format!( r#" {} {}/ {} {} {} "#, xml_escape(&config.title), base_url, base_url, base_url, updated, xml_escape(&config.author), entries, ) } /// Escape XML special characters. fn xml_escape(s: &str) -> String { s.replace('&', "&") .replace('<', "<") .replace('>', ">") .replace('"', """) .replace('\'', "'") } #[cfg(test)] mod tests { use super::*; #[test] fn test_xml_escape() { assert_eq!(xml_escape("Hello & World"), "Hello & World"); assert_eq!(xml_escape(""), "<tag>"); } }