Page Prerenderer
Generate static HTML pages at build time for list pages, index pages, and configured content.
On this page
PagePrerenderer generates static HTML pages at build time. Use this for list pages, index pages, and other content that doesn't come from markdown files.
When It Runs
PagePrerenderers run during the publish pipeline, after content is processed:
Content ingestion
|
Content rendering
|
=======================================
PagePrerenderer::prepare() <- You are here
=======================================
|
Template rendering
|
HTML output
The Trait
#[async_trait]
pub trait PagePrerenderer: Send + Sync {
fn page_type(&self) -> &str;
fn priority(&self) -> u32 {
100
}
async fn prepare(&self, ctx: &PagePrepareContext<'_>) -> Result<Option<PageRenderSpec>>;
}
PagePrepareContext
impl<'a> PagePrepareContext<'a> {
pub fn web_config(&self) -> &FullWebConfig;
pub fn config(&self) -> &ContentConfigRaw;
pub fn db_pool<T>(&self) -> Option<&T>;
}
PageRenderSpec
pub struct PageRenderSpec {
pub template_name: String,
pub template_data: Value,
pub output_path: PathBuf,
}
impl PageRenderSpec {
pub fn new(
template_name: impl Into<String>,
template_data: Value,
output_path: impl Into<PathBuf>,
) -> Self;
}
Basic Implementation
use systemprompt::template_provider::{PagePrepareContext, PagePrerenderer, PageRenderSpec};
use anyhow::Result;
use async_trait::async_trait;
use serde_json::json;
use std::path::PathBuf;
pub struct BlogListPrerenderer;
#[async_trait]
impl PagePrerenderer for BlogListPrerenderer {
fn page_type(&self) -> &str {
"blog-list"
}
fn priority(&self) -> u32 {
100
}
async fn prepare(&self, ctx: &PagePrepareContext<'_>) -> Result<Option<PageRenderSpec>> {
let pool = ctx.db_pool::<Arc<PgPool>>()
.ok_or_else(|| anyhow::anyhow!("Database not available"))?;
let posts = sqlx::query!(
r#"SELECT slug, title, description, published_at
FROM markdown_content
WHERE source_id = 'blog' AND public = true
ORDER BY published_at DESC"#
)
.fetch_all(&*pool)
.await?;
let posts_html = self.render_post_cards(&posts);
let template_data = json!({
"TITLE": "Blog",
"DESCRIPTION": "Latest posts from our blog",
"POSTS": posts_html,
"POST_COUNT": posts.len(),
});
Ok(Some(PageRenderSpec::new(
"blog-list",
template_data,
PathBuf::from("blog/index.html"),
)))
}
}
impl BlogListPrerenderer {
fn render_post_cards(&self, posts: &[Record]) -> String {
posts.iter()
.map(|p| format!(
r#"<article class="post-card">
<a href="/blog/{}">
<h3>{}</h3>
<p>{}</p>
</a>
</article>"#,
p.slug, p.title, p.description.as_deref().unwrap_or("")
))
.collect::<Vec<_>>()
.join("\n")
}
}
Returning None
Return None to skip rendering (e.g., when feature is disabled):
async fn prepare(&self, ctx: &PagePrepareContext<'_>) -> Result<Option<PageRenderSpec>> {
if !self.config.blog_list_enabled {
return Ok(None);
}
// ... render page
}
Registration
impl Extension for WebExtension {
fn page_prerenderers(&self) -> Vec<Arc<dyn PagePrerenderer>> {
vec![
Arc::new(HomepagePrerenderer::new(self.config.clone())),
Arc::new(BlogListPrerenderer),
Arc::new(DocsIndexPrerenderer),
Arc::new(SitemapPrerenderer),
]
}
}
Common Patterns
Homepage
async fn prepare(&self, ctx: &PagePrepareContext<'_>) -> Result<Option<PageRenderSpec>> {
let pool = ctx.db_pool::<Arc<PgPool>>()?;
let featured = self.fetch_featured_posts(pool).await?;
let recent = self.fetch_recent_posts(pool, 5).await?;
Ok(Some(PageRenderSpec::new(
"homepage",
json!({
"FEATURED_POSTS": featured,
"RECENT_POSTS": recent,
"HERO_TITLE": ctx.web_config().hero.title,
}),
PathBuf::from("index.html"),
)))
}
Documentation Index
async fn prepare(&self, ctx: &PagePrepareContext<'_>) -> Result<Option<PageRenderSpec>> {
let pool = ctx.db_pool::<Arc<PgPool>>()?;
let sections = self.fetch_doc_sections(pool).await?;
Ok(Some(PageRenderSpec::new(
"docs-index",
json!({
"TITLE": "Documentation",
"SECTIONS": sections,
}),
PathBuf::from("docs/index.html"),
)))
}
Multiple Pages
A single prerenderer can generate multiple pages by using a wrapper that calls it multiple times, or by returning specs for each variant.
Priority
Lower priority values indicate higher importance and execute first. When multiple prerenderers target the same page_type, only the first one (lowest priority value) runs - others are skipped.
| Priority | Use Case |
|---|---|
| 0-49 | Critical - overrides defaults |
| 50-99 | Core application pages |
| 100 | Default (fallback) |
| 101+ | Low priority - easily overridden |
fn priority(&self) -> u32 {
50 // Higher importance than default (100), will override core defaults
}
Example: If your extension's HomepagePrerenderer has priority 10 and the core's DefaultHomepagePrerenderer has priority 100, your prerenderer runs and the core's is skipped.