diff --git a/src/assets.rs b/src/assets.rs index fee2c08..0ac124a 100644 --- a/src/assets.rs +++ b/src/assets.rs @@ -6,6 +6,7 @@ use include_dir::{Dir, include_dir}; static CLIENT_ASSETS: Dir = include_dir!("$CARGO_MANIFEST_DIR/web/build/client"); static ERROR_PAGES: Dir = include_dir!("$CARGO_MANIFEST_DIR/web/build/prerendered/errors"); +static PRERENDERED_PAGES: Dir = include_dir!("$CARGO_MANIFEST_DIR/web/build/prerendered"); pub async fn serve_embedded_asset(uri: Uri) -> Response { let path = uri.path(); @@ -84,3 +85,56 @@ pub fn get_error_page(status_code: u16) -> Option<&'static [u8]> { let filename = format!("{}.html", status_code); ERROR_PAGES.get_file(&filename).map(|f| f.contents()) } + +/// Serve a prerendered page by path, if it exists. +/// +/// Prerendered pages are built by SvelteKit at compile time and embedded. +/// This handles various path patterns: +/// - `/path` → looks for `path.html` +/// - `/path/` → looks for `path.html` or `path/index.html` +/// +/// # Arguments +/// * `path` - Request path (e.g., "/pgp", "/about/") +/// +/// # Returns +/// * `Some(Response)` - HTML response if prerendered page exists +/// * `None` - If no prerendered page exists for this path +pub fn try_serve_prerendered_page(path: &str) -> Option { + let path = path.strip_prefix('/').unwrap_or(path); + let path = path.strip_suffix('/').unwrap_or(path); + + // Try direct HTML file first: "pgp" -> "pgp.html" + let html_filename = format!("{}.html", path); + if let Some(file) = PRERENDERED_PAGES.get_file(&html_filename) { + return Some(serve_html_response(file.contents())); + } + + // Try index.html pattern: "path" -> "path/index.html" + let index_filename = format!("{}/index.html", path); + if let Some(file) = PRERENDERED_PAGES.get_file(&index_filename) { + return Some(serve_html_response(file.contents())); + } + + // Try root index: "" -> "index.html" + if path.is_empty() { + if let Some(file) = PRERENDERED_PAGES.get_file("index.html") { + return Some(serve_html_response(file.contents())); + } + } + + None +} + +fn serve_html_response(content: &'static [u8]) -> Response { + let mut headers = axum::http::HeaderMap::new(); + headers.insert( + header::CONTENT_TYPE, + header::HeaderValue::from_static("text/html; charset=utf-8"), + ); + headers.insert( + header::CACHE_CONTROL, + header::HeaderValue::from_static("public, max-age=3600"), + ); + + (StatusCode::OK, headers, content).into_response() +} diff --git a/src/main.rs b/src/main.rs index 5ca6c0e..8095643 100644 --- a/src/main.rs +++ b/src/main.rs @@ -23,7 +23,7 @@ mod middleware; mod og; mod r2; mod tarpit; -use assets::{serve_embedded_asset, try_serve_embedded_asset}; +use assets::{serve_embedded_asset, try_serve_embedded_asset, try_serve_prerendered_page}; use config::{Args, ListenAddr}; use formatter::{CustomJsonFormatter, CustomPrettyFormatter}; use health::HealthChecker; @@ -1927,6 +1927,13 @@ async fn isr_handler(State(state): State>, req: Request) -> Respon // If not found in embedded assets, continue to proxy (might be in Bun's static dir) } + // Check if this is a prerendered page (routes with `export const prerender = true`) + // This handles pages like /pgp, /about, etc. that are pre-built at compile time + if let Some(response) = try_serve_prerendered_page(path) { + tracing::debug!(path = %path, "Serving prerendered page"); + return response; + } + let bun_url = if state.downstream_url.starts_with('/') || state.downstream_url.starts_with("./") { if query.is_empty() {