Content Data Provider
Implement ContentDataProvider::enrich_content() to add computed fields, related content, and database lookups to content items before page data runs.
On this page
ContentDataProvider enriches content items during contents_to_json(), before PageDataProviders run. Use this to add computed fields, related content, or database lookups.
When It Runs
Database Query
|
=======================================
ContentDataProvider::enrich_content() <- You are here
=======================================
|
PageDataProvider::provide_page_data()
|
ComponentRenderer::render()
|
Template rendering
The Trait
#[async_trait]
pub trait ContentDataProvider: Send + Sync {
fn provider_id(&self) -> &'static str;
fn applies_to_sources(&self) -> Vec<String> {
vec![]
}
fn priority(&self) -> u32 {
100
}
async fn enrich_content(
&self,
ctx: &ContentDataContext<'_>,
item: &mut Value,
) -> ProviderResult<()>;
}
ProviderResult<T> is Result<T, ProviderError> from systemprompt::traits. Map internal failures to ProviderError::Internal(String).
ContentDataContext
impl<'a> ContentDataContext<'a> {
pub fn content_id(&self) -> &str;
pub fn source_name(&self) -> &str;
pub fn db_pool<T: 'static>(&self) -> Option<&T>;
}
The generator passes the database handle as Arc<Database>, so downcast with ctx.db_pool::<Arc<Database>>() and call .pool() on it to get the sqlx::PgPool.
Basic Implementation
use std::sync::Arc;
use async_trait::async_trait;
use serde_json::{json, Value};
use systemprompt::database::Database;
use systemprompt::extension::prelude::{ContentDataContext, ContentDataProvider};
use systemprompt::traits::{ProviderError, ProviderResult};
pub struct DocsContentDataProvider;
#[async_trait]
impl ContentDataProvider for DocsContentDataProvider {
fn provider_id(&self) -> &'static str {
"docs-content-enricher"
}
fn applies_to_sources(&self) -> Vec<String> {
vec!["documentation".to_string()]
}
fn priority(&self) -> u32 {
100
}
async fn enrich_content(
&self,
ctx: &ContentDataContext<'_>,
item: &mut Value,
) -> ProviderResult<()> {
let slug = item.get("slug").and_then(|v| v.as_str()).unwrap_or("").to_string();
// Add computed field
let reading_time = self.calculate_reading_time(item);
if let Some(obj) = item.as_object_mut() {
obj.insert("reading_time".to_string(), json!(reading_time));
}
// Add children for index pages
if item.get("kind").and_then(|v| v.as_str()) == Some("docs-index") {
if let Some(db) = ctx.db_pool::<Arc<Database>>() {
let pool = db
.pool()
.ok_or_else(|| ProviderError::Internal("pool not initialized".into()))?;
let children = self.fetch_children(&pool, ctx.source_name(), &slug).await?;
if let Some(obj) = item.as_object_mut() {
obj.insert("children".to_string(), json!(children));
}
}
}
Ok(())
}
}
impl DocsContentDataProvider {
fn calculate_reading_time(&self, item: &Value) -> u32 {
let body = item.get("body").and_then(|v| v.as_str()).unwrap_or("");
let words = body.split_whitespace().count();
((words as f32) / 200.0).ceil() as u32
}
async fn fetch_children(
&self,
pool: &sqlx::PgPool,
source: &str,
parent_slug: &str,
) -> ProviderResult<Vec<Value>> {
let children = sqlx::query!(
r#"SELECT slug, title, description
FROM markdown_content
WHERE source_id = $1 AND slug LIKE $2
ORDER BY slug"#,
source,
format!("{}/%", parent_slug)
)
.fetch_all(pool)
.await
.map_err(|e| ProviderError::Internal(e.to_string()))?;
Ok(children.into_iter().map(|c| json!({
"slug": c.slug,
"title": c.title,
"description": c.description,
})).collect())
}
}
Targeting Sources
Use applies_to_sources() to run only for specific content sources:
fn applies_to_sources(&self) -> Vec<String> {
vec!["documentation".to_string(), "blog".to_string()]
}
Return an empty vector to run for ALL sources.
Registration
impl Extension for WebExtension {
fn content_data_providers(&self) -> Vec<Arc<dyn ContentDataProvider>> {
vec![
Arc::new(DocsContentDataProvider),
Arc::new(RelatedPostsProvider::new(self.pool.clone())),
]
}
}
Common Patterns
Related Content
async fn enrich_content(&self, ctx: &ContentDataContext<'_>, item: &mut Value) -> ProviderResult<()> {
let tags = item.get("tags").and_then(|v| v.as_array()).cloned();
if let (Some(tags), Some(db)) = (tags, ctx.db_pool::<Arc<Database>>()) {
let related = self.find_by_tags(db, &tags).await?;
if let Some(obj) = item.as_object_mut() {
obj.insert("related_posts".to_string(), json!(related));
}
}
Ok(())
}
Breadcrumbs
async fn enrich_content(&self, ctx: &ContentDataContext<'_>, item: &mut Value) -> ProviderResult<()> {
let slug = item.get("slug").and_then(|v| v.as_str()).unwrap_or("").to_string();
let breadcrumbs = self.build_breadcrumbs(&slug);
if let Some(obj) = item.as_object_mut() {
obj.insert("breadcrumbs".to_string(), json!(breadcrumbs));
}
Ok(())
}