Web Extensions
Build web extensions for page data, static generation, templates, and asset management.
On this page
Web extensions provide page data, static generation, component rendering, and asset management for the frontend. These capabilities are implemented as methods on the Extension trait. For the complete trait reference, see Lifecycle Hooks.
Extension Traits
Web-related traits:
| Trait | Purpose |
|---|---|
PageDataProvider |
Inject data into templates |
PagePrerenderer |
Static page generation |
RequiredAssets |
CSS/JS asset registration |
PageDataProvider
Inject data into Handlebars templates:
use systemprompt::traits::{PageDataProvider, PageDataContext};
use serde_json::Value;
impl PageDataProvider for MyExtension {
fn page_data_providers(&self) -> Vec<(&'static str, PageDataProviderFn)> {
vec![
("navigation", Box::new(|ctx| Box::pin(get_navigation(ctx)))),
("homepage", Box::new(|ctx| Box::pin(get_homepage(ctx)))),
]
}
}
async fn get_navigation(ctx: PageDataContext) -> Option<Value> {
let nav = load_navigation_config()?;
Some(serde_json::to_value(nav).ok()?)
}
async fn get_homepage(ctx: PageDataContext) -> Option<Value> {
// Only provide for homepage
if ctx.path != "/" {
return None;
}
let homepage = load_homepage_config()?;
Some(serde_json::to_value(homepage).ok()?)
}
Using Page Data in Templates
{{! navigation data }}
<nav>
{{#each navigation.items}}
<a href="{{this.url}}">{{this.label}}</a>
{{/each}}
</nav>
{{! homepage data }}
{{#if homepage}}
<h1>{{homepage.hero.title}}</h1>
<p>{{homepage.hero.subtitle}}</p>
{{/if}}
PagePrerenderer
Generate static HTML at build time:
use systemprompt::traits::{PagePrerenderer, PrerenderedPage};
impl PagePrerenderer for MyExtension {
fn prerender_pages(&self) -> Vec<PrerenderedPage> {
vec![
PrerenderedPage {
path: "/sitemap.xml".to_string(),
content: generate_sitemap(),
content_type: "application/xml".to_string(),
},
PrerenderedPage {
path: "/robots.txt".to_string(),
content: generate_robots(),
content_type: "text/plain".to_string(),
},
]
}
}
fn generate_sitemap() -> String {
let pages = get_all_pages();
let mut xml = r#"<?xml version="1.0" encoding="UTF-8"?>"#.to_string();
xml.push_str(r#"<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">"#);
for page in pages {
xml.push_str(&format!(
"<url><loc>https://example.com{}</loc></url>",
page.path
));
}
xml.push_str("</urlset>");
xml
}
RequiredAssets
Register CSS and JS files:
use systemprompt::traits::RequiredAssets;
impl RequiredAssets for MyExtension {
fn required_assets(&self) -> Vec<&'static str> {
vec![
"css/core/variables.css",
"css/core/base.css",
"css/components/header.css",
"css/components/footer.css",
"css/feature-base.css",
"js/main.js",
]
}
}
Assets are copied from storage/files/ to web/dist/ during the publish pipeline.
Asset Management
File Locations
storage/files/css/ <- Source CSS files
├── core/
│ ├── variables.css
│ └── base.css
├── components/
│ ├── header.css
│ └── footer.css
└── feature-base.css
web/dist/css/ <- Output (generated)
Registering Assets
- Create file in
storage/files/css/ - Register in
required_assets() - Run publish pipeline
systemprompt infra jobs run publish_pipeline
Asset Loading in Templates
Assets are loaded via the head partial:
{{! templates/partials/head.html }}
<head>
{{#each assets.css}}
<link rel="stylesheet" href="/css/{{this}}">
{{/each}}
{{#each assets.js}}
<script src="/js/{{this}}" defer></script>
{{/each}}
</head>
Complete Extension Example
use systemprompt::extension::Extension;
use systemprompt::traits::{
PageDataProvider, PagePrerenderer, RequiredAssets,
PageDataProviderFn, PrerenderedPage, PageDataContext
};
use serde_json::Value;
pub struct WebExtension;
impl Extension for WebExtension {
fn id(&self) -> &'static str { "web" }
fn name(&self) -> &'static str { "Web Content & Navigation" }
fn version(&self) -> &'static str { "1.0.0" }
fn description(&self) -> &'static str {
"Provides navigation, homepage data, and static assets"
}
}
impl PageDataProvider for WebExtension {
fn page_data_providers(&self) -> Vec<(&'static str, PageDataProviderFn)> {
vec![
("navigation", Box::new(|ctx| Box::pin(get_nav(ctx)))),
("homepage", Box::new(|ctx| Box::pin(get_home(ctx)))),
("footer", Box::new(|ctx| Box::pin(get_footer(ctx)))),
]
}
}
impl PagePrerenderer for WebExtension {
fn prerender_pages(&self) -> Vec<PrerenderedPage> {
vec![
PrerenderedPage {
path: "/sitemap.xml".to_string(),
content: generate_sitemap(),
content_type: "application/xml".to_string(),
},
]
}
}
impl RequiredAssets for WebExtension {
fn required_assets(&self) -> Vec<&'static str> {
vec![
"css/core/variables.css",
"css/core/base.css",
"css/components/header.css",
"css/feature-base.css",
]
}
}
// Register
inventory::submit! { WebExtension }
Configuration Loading
Load YAML configuration for page data:
use std::path::Path;
fn load_navigation_config() -> Option<NavigationConfig> {
let path = Path::new("services/web/config/navigation.yaml");
let content = std::fs::read_to_string(path).ok()?;
serde_yaml::from_str(&content).ok()
}
fn load_homepage_config() -> Option<HomepageConfig> {
let path = Path::new("services/web/config/homepage.yaml");
let content = std::fs::read_to_string(path).ok()?;
serde_yaml::from_str(&content).ok()
}
Project Structure
extensions/web/
├── Cargo.toml
├── src/
│ ├── lib.rs
│ ├── extension.rs # Trait implementations
│ ├── config.rs # YAML config loading
│ ├── config_loader.rs # Navigation, homepage loaders
│ └── providers/
│ ├── mod.rs
│ ├── navigation.rs
│ └── homepage.rs
└── schema/
services/web/config/ # YAML configuration
├── navigation.yaml
├── homepage.yaml
└── features/
└── my-feature.yaml
storage/files/css/ # CSS source files
└── ...
CLI Commands
Verify Assets
systemprompt web assets list
Publish Pipeline
systemprompt infra jobs run publish_pipeline
Preview Templates
systemprompt web templates preview homepage