Web Extensions

Build web extensions for page data, static generation, templates, and asset management.

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

  1. Create file in storage/files/css/
  2. Register in required_assets()
  3. 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