systemprompt_generator/sitemap/
default_provider.rs1use anyhow::{Result, anyhow};
2use async_trait::async_trait;
3use chrono::Utc;
4use std::collections::HashMap;
5use systemprompt_models::{AppPaths, ContentConfigRaw};
6use systemprompt_provider_contracts::{
7 PlaceholderMapping, SitemapContext, SitemapProvider, SitemapSourceSpec, SitemapUrlEntry,
8};
9use tokio::fs;
10
11#[derive(Debug)]
12pub struct DefaultSitemapProvider {
13 content_config: ContentConfigRaw,
14}
15
16impl DefaultSitemapProvider {
17 pub async fn new() -> Result<Self> {
18 let content_config = load_content_config().await?;
19 Ok(Self { content_config })
20 }
21
22 #[must_use]
23 pub const fn from_config(content_config: ContentConfigRaw) -> Self {
24 Self { content_config }
25 }
26}
27
28async fn load_content_config() -> Result<ContentConfigRaw> {
29 let paths = AppPaths::get().map_err(|e| anyhow!("{}", e))?;
30 let config_path = paths.system().content_config();
31
32 let yaml_content = fs::read_to_string(&config_path)
33 .await
34 .map_err(|e| anyhow!("Failed to read content config: {}", e))?;
35
36 serde_yaml::from_str(&yaml_content)
37 .map_err(|e| anyhow!("Failed to parse content config: {}", e))
38}
39
40#[async_trait]
41impl SitemapProvider for DefaultSitemapProvider {
42 fn provider_id(&self) -> &'static str {
43 "default-sitemap"
44 }
45
46 fn source_specs(&self) -> Vec<SitemapSourceSpec> {
47 self.content_config
48 .content_sources
49 .iter()
50 .filter(|(_, source)| source.enabled)
51 .filter_map(|(_, source)| {
52 source.sitemap.as_ref().and_then(|sitemap| {
53 sitemap.enabled.then(|| SitemapSourceSpec {
54 source_id: source.source_id.clone(),
55 url_pattern: sitemap.url_pattern.clone(),
56 placeholders: vec![PlaceholderMapping {
57 placeholder: "{slug}".to_string(),
58 field: "slug".to_string(),
59 }],
60 priority: sitemap.priority,
61 changefreq: sitemap.changefreq.clone(),
62 })
63 })
64 })
65 .collect()
66 }
67
68 fn static_urls(&self, base_url: &str) -> Vec<SitemapUrlEntry> {
69 let today = Utc::now().format("%Y-%m-%d").to_string();
70
71 self.content_config
72 .content_sources
73 .iter()
74 .filter(|(_, source)| source.enabled)
75 .filter_map(|(_, source)| {
76 source.sitemap.as_ref().and_then(|sitemap| {
77 sitemap.parent_route.as_ref().and_then(|parent| {
78 parent.enabled.then(|| SitemapUrlEntry {
79 loc: format!("{}{}", base_url, parent.url),
80 lastmod: today.clone(),
81 changefreq: parent.changefreq.clone(),
82 priority: parent.priority,
83 })
84 })
85 })
86 })
87 .collect()
88 }
89
90 async fn resolve_placeholders(
91 &self,
92 _ctx: &SitemapContext<'_>,
93 content: &serde_json::Value,
94 placeholders: &[PlaceholderMapping],
95 ) -> Result<HashMap<String, String>> {
96 let mut resolved = HashMap::new();
97
98 for mapping in placeholders {
99 if let Some(value) = content.get(&mapping.field) {
100 let string_value = match value {
101 serde_json::Value::String(s) => s.clone(),
102 _ => value.to_string().trim_matches('"').to_string(),
103 };
104 resolved.insert(mapping.placeholder.clone(), string_value);
105 }
106 }
107
108 Ok(resolved)
109 }
110}