systemprompt-content 0.2.2

Markdown content management, sources, and event tracking for systemprompt.io AI governance dashboards. Governed publishing pipeline for the MCP governance platform.
Documentation
use std::path::PathBuf;
use std::sync::Arc;

use anyhow::Result;
use async_trait::async_trait;
use systemprompt_cloud::constants::storage;
use systemprompt_models::ContentConfigRaw;
use systemprompt_models::services::ServicesConfig;
use systemprompt_provider_contracts::{
    PagePrepareContext, PagePrerenderer, PageRenderSpec, WebConfig,
};

fn resolve_content_raw<'a>(ctx: &'a PagePrepareContext<'_>) -> Result<&'a ContentConfigRaw> {
    if let Some(services) = ctx.content_config::<ServicesConfig>() {
        return Ok(&services.content.raw);
    }
    ctx.content_config::<ContentConfigRaw>()
        .ok_or_else(|| anyhow::anyhow!("ContentConfig not available in context"))
}

const PAGE_TYPE: &str = "homepage";
const TEMPLATE_NAME: &str = "homepage";
const OUTPUT_FILE: &str = "index.html";

#[derive(Debug, Clone, Copy, Default)]
pub struct DefaultHomepagePrerenderer;

impl DefaultHomepagePrerenderer {
    #[must_use]
    pub const fn new() -> Self {
        Self
    }

    fn extract_branding(
        web_config: &WebConfig,
        content_config: &ContentConfigRaw,
    ) -> HomepageBranding {
        let org = &content_config.metadata.structured_data.organization;
        let branding = &web_config.branding;

        HomepageBranding {
            org_name: org.name.clone(),
            org_url: org.url.clone(),
            org_logo: org.logo.clone(),
            logo_path: branding
                .logo
                .primary
                .svg
                .clone()
                .unwrap_or_else(String::new),
            favicon_path: branding.favicon.clone(),
            twitter_handle: branding.twitter_handle.clone(),
            display_sitename: branding.display_sitename,
        }
    }
}

struct HomepageBranding {
    org_name: String,
    org_url: String,
    org_logo: String,
    logo_path: String,
    favicon_path: String,
    twitter_handle: String,
    display_sitename: bool,
}

#[async_trait]
impl PagePrerenderer for DefaultHomepagePrerenderer {
    fn page_type(&self) -> &str {
        PAGE_TYPE
    }

    fn priority(&self) -> u32 {
        100
    }

    async fn prepare(&self, ctx: &PagePrepareContext<'_>) -> Result<Option<PageRenderSpec>> {
        let content_config = resolve_content_raw(ctx)?;

        let branding = Self::extract_branding(ctx.web_config, content_config);

        let base_data = serde_json::json!({
            "site": ctx.web_config,
            "ORG_NAME": branding.org_name,
            "ORG_URL": branding.org_url,
            "ORG_LOGO": branding.org_logo,
            "LOGO_PATH": branding.logo_path,
            "FAVICON_PATH": branding.favicon_path,
            "TWITTER_HANDLE": branding.twitter_handle,
            "DISPLAY_SITENAME": branding.display_sitename,
            "HEADER_CTA_URL": "/",
            "JS_BASE_PATH": format!("/{}", storage::JS),
            "CSS_BASE_PATH": format!("/{}", storage::CSS)
        });

        Ok(Some(PageRenderSpec::new(
            TEMPLATE_NAME,
            base_data,
            PathBuf::from(OUTPUT_FILE),
        )))
    }
}

pub fn default_homepage_prerenderer() -> Arc<dyn PagePrerenderer> {
    Arc::new(DefaultHomepagePrerenderer::new())
}