Template Data Extender
Implement TemplateDataExtender::extend() to make final edits to assembled template data, adding canonical URLs, OpenGraph tags, and JSON-LD after providers run.
On this page
TemplateDataExtender runs after PageDataProviders and ComponentRenderers, allowing you to make final modifications to the assembled template data.
When It Runs
ContentDataProvider::enrich_content()
|
PageDataProvider::provide_page_data()
|
ComponentRenderer::render()
|
=======================================
TemplateDataExtender::extend() <- You are here
=======================================
|
Handlebars template rendering
The Trait
#[async_trait]
pub trait TemplateDataExtender: Send + Sync {
fn extender_id(&self) -> &str;
fn applies_to(&self) -> Vec<String> {
vec![]
}
async fn extend(
&self,
ctx: &ExtenderContext<'_>,
data: &mut Value,
) -> ProviderResult<()>;
fn priority(&self) -> u32 {
100
}
}
ExtenderContext
pub struct ExtenderContext<'a> {
pub item: &'a Value,
pub all_items: &'a [Value],
pub config: &'a serde_yaml::Value,
pub web_config: &'a WebConfig,
pub content_html: &'a str,
pub url_pattern: &'a str,
pub source_name: &'a str,
// private: db_pool
}
impl<'a> ExtenderContext<'a> {
pub fn db_pool<T: 'static>(&self) -> Option<&T>;
}
Basic Implementation
use async_trait::async_trait;
use serde_json::{json, Value};
use systemprompt::models::Config;
use systemprompt::template_provider::{ExtenderContext, TemplateDataExtender};
use systemprompt::traits::ProviderError;
pub struct CanonicalUrlExtender;
#[async_trait]
impl TemplateDataExtender for CanonicalUrlExtender {
fn extender_id(&self) -> &str {
"canonical-url"
}
fn applies_to(&self) -> Vec<String> {
vec![] // All content types
}
fn priority(&self) -> u32 {
100
}
async fn extend(
&self,
ctx: &ExtenderContext<'_>,
data: &mut Value,
) -> Result<(), ProviderError> {
let slug = ctx.item.get("slug").and_then(|v| v.as_str()).unwrap_or("");
let canonical = ctx.url_pattern.replace("{slug}", slug);
let config = Config::get()
.map_err(|e| ProviderError::Internal(e.to_string()))?;
if let Some(obj) = data.as_object_mut() {
obj.insert("CANONICAL_PATH".to_string(), json!(canonical));
obj.insert("CANONICAL_URL".to_string(), json!(format!(
"{}{}",
config.api_external_url,
canonical
)));
}
Ok(())
}
}
Targeting Content Types
fn applies_to(&self) -> Vec<String> {
vec!["blog".to_string(), "docs".to_string()]
}
Empty vector = all content types.
Registration
impl Extension for WebExtension {
fn template_data_extenders(&self) -> Vec<Arc<dyn TemplateDataExtender>> {
vec![
Arc::new(CanonicalUrlExtender),
Arc::new(OpenGraphExtender),
Arc::new(JsonLdExtender),
]
}
}
Common Patterns
OpenGraph Metadata
async fn extend(&self, ctx: &ExtenderContext<'_>, data: &mut Value) -> Result<(), ProviderError> {
let title = data.get("TITLE").and_then(|v| v.as_str()).unwrap_or("");
let description = data.get("DESCRIPTION").and_then(|v| v.as_str()).unwrap_or("");
let image = ctx.item.get("image").and_then(|v| v.as_str());
let config = Config::get()
.map_err(|e| ProviderError::Internal(e.to_string()))?;
if let Some(obj) = data.as_object_mut() {
obj.insert("OG_TITLE".to_string(), json!(title));
obj.insert("OG_DESCRIPTION".to_string(), json!(description));
if let Some(img) = image {
obj.insert("OG_IMAGE".to_string(), json!(format!("{}{}", config.api_external_url, img)));
}
}
Ok(())
}
JSON-LD Structured Data
async fn extend(&self, ctx: &ExtenderContext<'_>, data: &mut Value) -> Result<(), ProviderError> {
let json_ld = json!({
"@context": "https://schema.org",
"@type": "Article",
"headline": data.get("TITLE"),
"description": data.get("DESCRIPTION"),
"author": {
"@type": "Person",
"name": data.get("AUTHOR")
}
});
if let Some(obj) = data.as_object_mut() {
let serialized = serde_json::to_string(&json_ld)
.map_err(|e| ProviderError::Internal(e.to_string()))?;
obj.insert("JSON_LD".to_string(), json!(serialized));
}
Ok(())
}
Conditional Fields
async fn extend(&self, ctx: &ExtenderContext<'_>, data: &mut Value) -> Result<(), ProviderError> {
let has_toc = ctx.content_html.contains("<h2");
if let Some(obj) = data.as_object_mut() {
obj.insert("SHOW_TOC".to_string(), json!(has_toc));
}
Ok(())
}