systemprompt_generator/prerender/
engine.rs1use std::collections::HashSet;
10use std::path::PathBuf;
11
12use systemprompt_content::ContentRepository;
13use systemprompt_database::DbPool;
14use systemprompt_identifiers::LocaleCode;
15use systemprompt_models::AppPaths;
16use systemprompt_template_provider::{
17 ComponentContext, DynPagePrerenderer, PageContext, PagePrepareContext,
18};
19use tokio::fs;
20
21use crate::error::{GeneratorResult as Result, PublishError};
22use crate::prerender::content::process_all_sources;
23use crate::prerender::context::{PrerenderContext, load_prerender_context};
24use crate::prerender::utils::{merge_json_data, render_components};
25
26pub async fn prerender_content(
27 db_pool: DbPool,
28 content_repo: ContentRepository,
29 paths: &AppPaths,
30) -> Result<()> {
31 let ctx = load_prerender_context(db_pool, content_repo, paths).await?;
32 let total_rendered = process_all_sources(&ctx).await?;
33 tracing::info!(items_rendered = total_rendered, "Prerendering completed");
34 Ok(())
35}
36
37#[derive(Debug)]
38pub struct PagePrerenderResult {
39 pub page_type: String,
40 pub output_path: PathBuf,
41}
42
43pub async fn prerender_pages(
44 db_pool: DbPool,
45 content_repo: ContentRepository,
46 paths: &AppPaths,
47) -> Result<Vec<PagePrerenderResult>> {
48 let ctx = load_prerender_context(db_pool, content_repo, paths).await?;
49 prerender_pages_with_context(&ctx).await
50}
51
52async fn prerender_pages_with_context(ctx: &PrerenderContext) -> Result<Vec<PagePrerenderResult>> {
53 let prerenderers = ctx.template_registry.page_prerenderers();
54
55 if prerenderers.is_empty() {
56 tracing::warn!("No page prerenderers registered - no pages will be rendered");
57 return Ok(Vec::new());
58 }
59
60 let prerenderer_count = prerenderers.len();
61 let page_types: Vec<_> = prerenderers.iter().map(|p| p.page_type()).collect();
62 tracing::info!(
63 count = prerenderer_count,
64 page_types = ?page_types,
65 "Discovered page prerenderers"
66 );
67
68 let mut results = Vec::new();
69
70 for locale in &ctx.web_config.i18n.supported_locales {
71 let locale_prefix = ctx.web_config.i18n.locale_prefix(locale);
72 let prepare_ctx =
73 PagePrepareContext::new(&ctx.web_config, &ctx.config, &ctx.db_pool, &ctx.dist_dir)
74 .with_locale(locale);
75
76 let mut rendered_page_types: HashSet<String> = HashSet::new();
77
78 for prerenderer in prerenderers {
79 let page_type = prerenderer.page_type();
80
81 if rendered_page_types.contains(page_type) {
82 tracing::debug!(
83 page_type = %page_type,
84 locale = %locale,
85 priority = prerenderer.priority(),
86 "Skipping prerenderer, page type already rendered by higher-priority prerenderer"
87 );
88 continue;
89 }
90
91 let render = RenderPageParams {
92 ctx,
93 prerenderer,
94 prepare_ctx: &prepare_ctx,
95 locale,
96 locale_prefix: &locale_prefix,
97 };
98
99 if let Some(result) = render_prerenderer_page(&render).await? {
100 rendered_page_types.insert(page_type.to_owned());
101 results.push(result);
102 }
103 }
104 }
105
106 Ok(results)
107}
108
109struct RenderPageParams<'a> {
110 ctx: &'a PrerenderContext,
111 prerenderer: &'a DynPagePrerenderer,
112 prepare_ctx: &'a PagePrepareContext<'a>,
113 locale: &'a LocaleCode,
114 locale_prefix: &'a str,
115}
116
117async fn render_prerenderer_page(
118 params: &RenderPageParams<'_>,
119) -> Result<Option<PagePrerenderResult>> {
120 let RenderPageParams {
121 ctx,
122 prerenderer,
123 prepare_ctx,
124 locale,
125 locale_prefix,
126 } = params;
127
128 let page_type = prerenderer.page_type();
129
130 let render_spec = prerenderer
131 .prepare(prepare_ctx)
132 .await
133 .map_err(|e| PublishError::page_prerenderer_failed(page_type, e.to_string()))?;
134
135 let Some(spec) = render_spec else {
136 tracing::debug!(page_type = %page_type, locale = %locale, "Prerenderer returned None, skipping");
137 return Ok(None);
138 };
139
140 if !ctx.template_registry.has_template(&spec.template_name) {
141 tracing::warn!(
142 page_type = %page_type,
143 template = %spec.template_name,
144 "Template not found, skipping page"
145 );
146 return Ok(None);
147 }
148
149 let page_data = collect_page_data(ctx, page_type, locale, spec.base_data).await?;
150
151 let html = ctx
152 .template_registry
153 .render(&spec.template_name, &page_data)
154 .map_err(|e| PublishError::render_failed(&spec.template_name, None, e.to_string()))?;
155
156 let prefixed_output = if locale_prefix.is_empty() {
157 spec.output_path.clone()
158 } else {
159 PathBuf::from(locale_prefix.trim_start_matches('/')).join(&spec.output_path)
160 };
161 let output_path = ctx.dist_dir.join(&prefixed_output);
162
163 if let Some(parent) = output_path.parent() {
164 fs::create_dir_all(parent).await?;
165 }
166
167 fs::write(&output_path, html).await?;
168
169 tracing::info!(
170 page_type = %page_type,
171 locale = %locale,
172 path = %output_path.display(),
173 "Generated page"
174 );
175
176 Ok(Some(PagePrerenderResult {
177 page_type: page_type.to_owned(),
178 output_path,
179 }))
180}
181
182async fn collect_page_data(
183 ctx: &PrerenderContext,
184 page_type: &str,
185 locale: &LocaleCode,
186 base_data: serde_json::Value,
187) -> Result<serde_json::Value> {
188 let mut page_data = base_data;
189 if let Some(obj) = page_data.as_object_mut() {
190 obj.insert(
191 "locale".to_owned(),
192 serde_json::Value::String(locale.to_string()),
193 );
194 }
195
196 let page_ctx =
197 PageContext::new(page_type, &ctx.web_config, &ctx.config, &ctx.db_pool).with_locale(locale);
198 let providers = ctx.template_registry.page_providers_for(page_type);
199 let provider_ids: Vec<_> = providers.iter().map(|p| p.provider_id()).collect();
200
201 tracing::debug!(
202 page_type = %page_type,
203 locale = %locale,
204 provider_count = providers.len(),
205 provider_ids = ?provider_ids,
206 "Collecting page data from providers"
207 );
208
209 for provider in &providers {
210 let data = provider
211 .provide_page_data(&page_ctx)
212 .await
213 .map_err(|e| PublishError::provider_failed(provider.provider_id(), e.to_string()))?;
214 merge_json_data(&mut page_data, &data);
215 }
216
217 let component_ctx = ComponentContext::for_page(&ctx.web_config);
218 render_components(
219 &ctx.template_registry,
220 page_type,
221 &component_ctx,
222 &mut page_data,
223 )
224 .await;
225
226 Ok(page_data)
227}