Replace hardcoded "/blog/{slug}.html" URL pattern with dynamic
path derivation using Content.output_path(). This ensures feed
URLs work correctly for any content location, not just blog posts.
97 lines
2.5 KiB
Rust
97 lines
2.5 KiB
Rust
//! 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#" <entry>
|
|
<title>{}</title>
|
|
<link href="{}" rel="alternate"/>
|
|
<id>{}</id>
|
|
<updated>{}</updated>
|
|
<summary>{}</summary>
|
|
</entry>
|
|
"#,
|
|
xml_escape(&post.frontmatter.title),
|
|
post_url,
|
|
post_url,
|
|
post_date,
|
|
summary,
|
|
));
|
|
}
|
|
|
|
format!(
|
|
r#"<?xml version="1.0" encoding="utf-8"?>
|
|
<feed xmlns="http://www.w3.org/2005/Atom">
|
|
<title>{}</title>
|
|
<link href="{}" rel="alternate"/>
|
|
<link href="{}/feed.xml" rel="self"/>
|
|
<id>{}/</id>
|
|
<updated>{}</updated>
|
|
<author>
|
|
<name>{}</name>
|
|
</author>
|
|
{}
|
|
</feed>
|
|
"#,
|
|
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>"), "<tag>");
|
|
}
|
|
}
|