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