This playbook walks you through creating a PageDataProvider that supplies template variables for your pages.
Prerequisites
- Existing extension crate in
extensions/ - Understanding of what template variables your templates need
- Content type(s) you want to target
Step 1: Define Your Provider Struct
Create a new file in your extension's providers/ directory:
use systemprompt::extension::prelude::{PageContext, PageDataProvider};
use anyhow::Result;
use async_trait::async_trait;
use serde_json::{json, Value};
pub struct ContentPageDataProvider;
impl ContentPageDataProvider {
pub fn new() -> Self {
Self
}
}
Step 2: Implement the Trait
Implement PageDataProvider:
#[async_trait]
impl PageDataProvider for ContentPageDataProvider {
fn provider_id(&self) -> &'static str {
"content-page-data"
}
fn applies_to_pages(&self) -> Vec<String> {
vec![]
}
async fn provide_page_data(&self, ctx: &PageContext<'_>) -> Result<Value> {
let item = ctx.content_item()
.ok_or_else(|| anyhow::anyhow!("No content item available"))?;
let title = item.get("title")
.and_then(|v| v.as_str())
.unwrap_or("");
let description = item.get("description")
.or_else(|| item.get("excerpt"))
.and_then(|v| v.as_str())
.unwrap_or("");
Ok(json!({
"TITLE": title,
"DESCRIPTION": description,
}))
}
}
Step 3: Add Date Formatting
Most content needs formatted dates:
async fn provide_page_data(&self, ctx: &PageContext<'_>) -> Result<Value> {
let item = ctx.content_item()
.ok_or_else(|| anyhow::anyhow!("No content item"))?;
let title = item.get("title").and_then(|v| v.as_str()).unwrap_or("");
let description = item.get("description").and_then(|v| v.as_str()).unwrap_or("");
let published = item.get("published_at")
.or_else(|| item.get("date"))
.and_then(|v| v.as_str());
let (formatted_date, iso_date) = match published {
Some(date_str) => {
match chrono::DateTime::parse_from_rfc3339(date_str) {
Ok(dt) => (
dt.format("%B %d, %Y").to_string(),
dt.format("%Y-%m-%d").to_string(),
),
Err(_) => (date_str.to_string(), date_str.to_string()),
}
}
None => (String::new(), String::new()),
};
Ok(json!({
"TITLE": title,
"DESCRIPTION": description,
"DATE": formatted_date,
"DATE_ISO": iso_date,
}))
}
Step 4: Add Author and Keywords
Extract additional metadata:
async fn provide_page_data(&self, ctx: &PageContext<'_>) -> Result<Value> {
let item = ctx.content_item()
.ok_or_else(|| anyhow::anyhow!("No content item"))?;
let title = item.get("title").and_then(|v| v.as_str()).unwrap_or("");
let description = item.get("description").and_then(|v| v.as_str()).unwrap_or("");
let author = item.get("author").and_then(|v| v.as_str()).unwrap_or("");
let keywords = item.get("keywords")
.or_else(|| item.get("tags"))
.cloned()
.unwrap_or(json!([]));
let published = item.get("published_at")
.or_else(|| item.get("date"))
.and_then(|v| v.as_str());
let (formatted_date, iso_date) = format_date(published);
Ok(json!({
"TITLE": title,
"DESCRIPTION": description,
"AUTHOR": author,
"KEYWORDS": keywords,
"DATE": formatted_date,
"DATE_ISO": iso_date,
}))
}
fn format_date(date_str: Option<&str>) -> (String, String) {
match date_str {
Some(s) => chrono::DateTime::parse_from_rfc3339(s)
.map(|dt| (
dt.format("%B %d, %Y").to_string(),
dt.format("%Y-%m-%d").to_string(),
))
.unwrap_or_else(|_| (s.to_string(), s.to_string())),
None => (String::new(), String::new()),
}
}
Step 5: Target Specific Content Types
To only run for specific pages:
fn applies_to_pages(&self) -> Vec<String> {
vec!["blog".to_string(), "docs".to_string()]
}
For list pages, the content type is {source}-list:
fn applies_to_pages(&self) -> Vec<String> {
vec!["blog-list".to_string(), "documentation-list".to_string()]
}
Step 6: Register in Extension
Add to your extension's page_data_providers():
impl Extension for WebExtension {
fn page_data_providers(&self) -> Vec<Arc<dyn PageDataProvider>> {
vec![
Arc::new(ContentPageDataProvider::new()),
]
}
}
Step 7: Export from Module
Update providers/mod.rs:
mod content_page;
pub use content_page::ContentPageDataProvider;
Step 8: Verify Template Usage
Check your templates use the variables:
<head>
<title>{{TITLE}}</title>
<meta name="description" content="{{DESCRIPTION}}">
<meta name="author" content="{{AUTHOR}}">
</head>
<article>
<h1>{{TITLE}}</h1>
<time datetime="{{DATE_ISO}}">{{DATE}}</time>
<p class="author">By {{AUTHOR}}</p>
</article>
Step 9: Test
Run the publish pipeline to verify:
systemprompt infra jobs run publish_pipeline
Check generated HTML for your variables.
Complete Example
use systemprompt::extension::prelude::{PageContext, PageDataProvider};
use anyhow::Result;
use async_trait::async_trait;
use serde_json::{json, Value};
pub struct ContentPageDataProvider;
impl ContentPageDataProvider {
pub fn new() -> Self {
Self
}
fn format_date(date_str: Option<&str>) -> (String, String) {
match date_str {
Some(s) => chrono::DateTime::parse_from_rfc3339(s)
.map(|dt| (
dt.format("%B %d, %Y").to_string(),
dt.format("%Y-%m-%d").to_string(),
))
.unwrap_or_else(|_| (s.to_string(), s.to_string())),
None => (String::new(), String::new()),
}
}
}
#[async_trait]
impl PageDataProvider for ContentPageDataProvider {
fn provider_id(&self) -> &'static str {
"content-page-data"
}
fn applies_to_pages(&self) -> Vec<String> {
vec![]
}
async fn provide_page_data(&self, ctx: &PageContext<'_>) -> Result<Value> {
let item = ctx.content_item()
.ok_or_else(|| anyhow::anyhow!("No content item"))?;
let title = item.get("title").and_then(|v| v.as_str()).unwrap_or("");
let description = item.get("description")
.or_else(|| item.get("excerpt"))
.and_then(|v| v.as_str())
.unwrap_or("");
let author = item.get("author").and_then(|v| v.as_str()).unwrap_or("");
let keywords = item.get("keywords")
.or_else(|| item.get("tags"))
.cloned()
.unwrap_or(json!([]));
let published = item.get("published_at")
.or_else(|| item.get("date"))
.and_then(|v| v.as_str());
let (formatted_date, iso_date) = Self::format_date(published);
Ok(json!({
"TITLE": title,
"DESCRIPTION": description,
"AUTHOR": author,
"KEYWORDS": keywords,
"DATE": formatted_date,
"DATE_ISO": iso_date,
}))
}
}
Checklist
- Created provider struct
- Implemented PageDataProvider trait
- Set correct
provider_id() - Configured
applies_to_pages()targeting - Extracted all needed fields from content item
- Added date formatting
- Registered in extension
- Exported from module
- Verified template variables
- Tested with publish pipeline