Skip to main content

systemprompt_content/
homepage_prerenderer.rs

1//! Prerenders the homepage from content config into `index.html`.
2//!
3//! Copyright (c) systemprompt.io — Business Source License 1.1.
4//! See <https://systemprompt.io> for licensing details.
5
6use std::path::PathBuf;
7use std::sync::Arc;
8
9use async_trait::async_trait;
10use systemprompt_cloud::constants::storage;
11use systemprompt_models::ContentConfigRaw;
12use systemprompt_provider_contracts::{
13    PagePrepareContext, PagePrerenderer, PageRenderSpec, ProviderError, ProviderResult, WebConfig,
14};
15
16fn resolve_content_raw<'a>(
17    ctx: &'a PagePrepareContext<'_>,
18) -> ProviderResult<&'a ContentConfigRaw> {
19    ctx.content_config::<ContentConfigRaw>().ok_or_else(|| {
20        ProviderError::Configuration("ContentConfig not available in context".into())
21    })
22}
23
24const PAGE_TYPE: &str = "homepage";
25const TEMPLATE_NAME: &str = "homepage";
26const OUTPUT_FILE: &str = "index.html";
27
28#[derive(Debug, Clone, Copy, Default)]
29pub struct DefaultHomepagePrerenderer;
30
31impl DefaultHomepagePrerenderer {
32    #[must_use]
33    pub const fn new() -> Self {
34        Self
35    }
36
37    fn extract_branding(
38        web_config: &WebConfig,
39        content_config: &ContentConfigRaw,
40    ) -> HomepageBranding {
41        let org = &content_config.metadata.structured_data.organization;
42        let branding = &web_config.branding;
43
44        HomepageBranding {
45            org_name: org.name.clone(),
46            org_url: org.url.clone(),
47            org_logo: org.logo.clone(),
48            logo_path: branding
49                .logo
50                .primary
51                .svg
52                .clone()
53                .unwrap_or_else(String::new),
54            favicon_path: branding.favicon.clone(),
55            twitter_handle: branding.twitter_handle.clone(),
56            display_sitename: branding.display_sitename,
57        }
58    }
59}
60
61struct HomepageBranding {
62    org_name: String,
63    org_url: String,
64    org_logo: String,
65    logo_path: String,
66    favicon_path: String,
67    twitter_handle: String,
68    display_sitename: bool,
69}
70
71#[async_trait]
72impl PagePrerenderer for DefaultHomepagePrerenderer {
73    fn page_type(&self) -> &str {
74        PAGE_TYPE
75    }
76
77    fn priority(&self) -> u32 {
78        100
79    }
80
81    async fn prepare(
82        &self,
83        ctx: &PagePrepareContext<'_>,
84    ) -> ProviderResult<Option<PageRenderSpec>> {
85        let content_config = resolve_content_raw(ctx)?;
86
87        let branding = Self::extract_branding(ctx.web_config, content_config);
88
89        let base_data = serde_json::json!({
90            "site": ctx.web_config,
91            "ORG_NAME": branding.org_name,
92            "ORG_URL": branding.org_url,
93            "ORG_LOGO": branding.org_logo,
94            "LOGO_PATH": branding.logo_path,
95            "FAVICON_PATH": branding.favicon_path,
96            "TWITTER_HANDLE": branding.twitter_handle,
97            "DISPLAY_SITENAME": branding.display_sitename,
98            "HEADER_CTA_URL": "/",
99            "JS_BASE_PATH": format!("/{}", storage::JS),
100            "CSS_BASE_PATH": format!("/{}", storage::CSS)
101        });
102
103        Ok(Some(PageRenderSpec::new(
104            TEMPLATE_NAME,
105            base_data,
106            PathBuf::from(OUTPUT_FILE),
107        )))
108    }
109}
110
111pub fn default_homepage_prerenderer() -> Arc<dyn PagePrerenderer> {
112    Arc::new(DefaultHomepagePrerenderer::new())
113}