RSS & Sitemap Providers
Implement RssFeedProvider and SitemapProvider to emit RSS feeds and sitemap URL entries during the publish pipeline via feed_specs() and source_specs().
On this page
RssFeedProvider and SitemapProvider generate RSS feeds and sitemap entries during the publish pipeline. Both traits live in systemprompt-provider-contracts and are re-exported through systemprompt::extension::prelude.
RssFeedProvider
The Trait
#[async_trait]
pub trait RssFeedProvider: Send + Sync {
fn provider_id(&self) -> &'static str;
fn feed_specs(&self) -> Vec<RssFeedSpec>;
async fn feed_metadata(&self, ctx: &RssFeedContext<'_>) -> ProviderResult<RssFeedMetadata>;
async fn fetch_items(
&self,
ctx: &RssFeedContext<'_>,
limit: i64,
) -> ProviderResult<Vec<RssFeedItem>>;
fn priority(&self) -> u32 {
100
}
}
feed_specs() declares which feeds this provider emits. For each spec, the generator calls feed_metadata() for the channel header and fetch_items() for the entries.
RssFeedContext
pub struct RssFeedContext<'a> {
pub base_url: &'a str,
pub source_name: &'a str,
}
RssFeedSpec
pub struct RssFeedSpec {
pub source_id: SourceId,
pub max_items: i64,
pub output_filename: String,
}
pub struct RssFeedMetadata {
pub title: String,
pub link: String,
pub description: String,
pub language: Option<String>,
}
pub struct RssFeedItem {
pub title: String,
pub link: String,
pub description: String,
pub pub_date: DateTime<Utc>,
pub guid: String,
pub author: Option<String>,
}
Implementation
use systemprompt::extension::prelude::*;
use systemprompt::identifiers::SourceId;
use systemprompt_provider_contracts::ProviderResult;
pub struct BlogRssProvider;
#[async_trait]
impl RssFeedProvider for BlogRssProvider {
fn provider_id(&self) -> &'static str {
"blog-rss"
}
fn feed_specs(&self) -> Vec<RssFeedSpec> {
vec![RssFeedSpec {
source_id: SourceId::new("blog"),
max_items: 20,
output_filename: "blog.xml".to_string(),
}]
}
async fn feed_metadata(&self, ctx: &RssFeedContext<'_>) -> ProviderResult<RssFeedMetadata> {
Ok(RssFeedMetadata {
title: "Blog".to_string(),
link: format!("{}/blog", ctx.base_url),
description: "Latest posts".to_string(),
language: Some("en".to_string()),
})
}
async fn fetch_items(
&self,
ctx: &RssFeedContext<'_>,
limit: i64,
) -> ProviderResult<Vec<RssFeedItem>> {
// Load your latest content (via a repository method, never inline SQL
// in business logic) and map each row to an RssFeedItem.
let posts = load_latest_posts(limit).await?;
Ok(posts
.into_iter()
.map(|p| RssFeedItem {
title: p.title,
link: format!("{}/blog/{}", ctx.base_url, p.slug),
description: p.description,
pub_date: p.published_at,
guid: format!("{}/blog/{}", ctx.base_url, p.slug),
author: None,
})
.collect())
}
}
The core ships DefaultRssFeedProvider, which emits one feed per enabled content source from services/content/config.yaml. Implement your own provider only when you need feeds the config-driven default cannot express.
SitemapProvider
The Trait
#[async_trait]
pub trait SitemapProvider: Send + Sync {
fn provider_id(&self) -> &'static str;
fn source_specs(&self) -> Vec<SitemapSourceSpec> {
vec![]
}
fn static_urls(&self, base_url: &str) -> Vec<SitemapUrlEntry> {
vec![]
}
async fn resolve_placeholders(
&self,
ctx: &SitemapContext<'_>,
content: &serde_json::Value,
placeholders: &[PlaceholderMapping],
) -> ProviderResult<HashMap<String, String>>;
fn priority(&self) -> u32 {
100
}
}
source_specs() declares URL patterns per content source; the generator iterates that source's content and calls resolve_placeholders() to fill each pattern. static_urls() adds fixed entries such as landing pages.
SitemapContext
pub struct SitemapContext<'a> {
pub base_url: &'a str,
pub source_name: &'a str,
}
SitemapSourceSpec
pub struct SitemapSourceSpec {
pub source_id: SourceId,
pub url_pattern: String,
pub placeholders: Vec<PlaceholderMapping>,
pub priority: f32,
pub changefreq: String,
}
pub struct PlaceholderMapping {
pub placeholder: String,
pub field: String,
}
pub struct SitemapUrlEntry {
pub loc: String,
pub lastmod: String,
pub changefreq: String,
pub priority: f32,
pub alternates: Vec<SitemapAlternate>,
}
Implementation
use std::collections::HashMap;
use systemprompt::extension::prelude::*;
use systemprompt::identifiers::SourceId;
use systemprompt_provider_contracts::ProviderResult;
pub struct ContentSitemapProvider;
#[async_trait]
impl SitemapProvider for ContentSitemapProvider {
fn provider_id(&self) -> &'static str {
"content-sitemap"
}
fn source_specs(&self) -> Vec<SitemapSourceSpec> {
vec![SitemapSourceSpec {
source_id: SourceId::new("blog"),
url_pattern: "/blog/{slug}".to_string(),
placeholders: vec![PlaceholderMapping {
placeholder: "{slug}".to_string(),
field: "slug".to_string(),
}],
priority: 0.7,
changefreq: "weekly".to_string(),
}]
}
async fn resolve_placeholders(
&self,
_ctx: &SitemapContext<'_>,
content: &serde_json::Value,
placeholders: &[PlaceholderMapping],
) -> ProviderResult<HashMap<String, String>> {
let mut resolved = HashMap::new();
for mapping in placeholders {
if let Some(value) = content.get(&mapping.field) {
let string_value = match value {
serde_json::Value::String(s) => s.clone(),
other => other.to_string().trim_matches('"').to_owned(),
};
resolved.insert(mapping.placeholder.clone(), string_value);
}
}
Ok(resolved)
}
}
The core ships DefaultSitemapProvider, driven entirely by the sitemap: block on each content source in services/content/config.yaml (including parent_route static entries). Prefer configuring that over writing a custom provider.
Registration
impl Extension for WebExtension {
fn rss_feed_providers(&self) -> Vec<Arc<dyn RssFeedProvider>> {
vec![Arc::new(BlogRssProvider)]
}
fn sitemap_providers(&self) -> Vec<Arc<dyn SitemapProvider>> {
vec![Arc::new(ContentSitemapProvider)]
}
}
Multiple Feeds
Return multiple specs from a single provider; the generator calls feed_metadata() and fetch_items() once per spec:
fn feed_specs(&self) -> Vec<RssFeedSpec> {
vec![
RssFeedSpec {
source_id: SourceId::new("blog"),
max_items: 20,
output_filename: "blog.xml".to_string(),
},
RssFeedSpec {
source_id: SourceId::new("documentation"),
max_items: 20,
output_filename: "documentation.xml".to_string(),
},
]
}