diff --git a/crates/tuono/src/app.rs b/crates/tuono/src/app.rs index 4fe6c764..5421cfe6 100644 --- a/crates/tuono/src/app.rs +++ b/crates/tuono/src/app.rs @@ -120,6 +120,7 @@ mod tests { "/home/user/Documents/tuono/src/routes/posts/index.rs", "/home/user/Documents/tuono/src/routes/posts/[post].rs", "/home/user/Documents/tuono/src/routes/posts/UPPERCASE.rs", + "/home/user/Documents/tuono/src/routes/sitemap.xml.rs", ]; routes @@ -132,6 +133,7 @@ mod tests { ("/posts/index", "posts_index"), ("/posts/[post]", "posts_dyn_post"), ("/posts/UPPERCASE", "posts_uppercase"), + ("/sitemap.xml", "sitemap_dot_xml"), ]; results.into_iter().for_each(|(path, module_import)| { diff --git a/crates/tuono/src/cli.rs b/crates/tuono/src/cli.rs index 54d6351a..9b0a105c 100644 --- a/crates/tuono/src/cli.rs +++ b/crates/tuono/src/cli.rs @@ -105,7 +105,7 @@ pub fn app() -> std::io::Result<()> { } for (_, route) in app.route_map { - route.save_ssg_html(&reqwest) + route.save_ssg_file(&reqwest) } // Close server diff --git a/crates/tuono/src/route.rs b/crates/tuono/src/route.rs index 2abaf76c..a5360b45 100644 --- a/crates/tuono/src/route.rs +++ b/crates/tuono/src/route.rs @@ -27,9 +27,16 @@ impl AxumInfo { let axum_route = path.replace("/index", ""); + let module_import = module + .as_str() + .to_string() + .replace('/', "_") + .replace('.', "_dot_") + .to_lowercase(); + if axum_route.is_empty() { return AxumInfo { - module_import: module.as_str().to_string().replace('/', "_"), + module_import, axum_route: "/".to_string(), }; } @@ -47,12 +54,15 @@ impl AxumInfo { } AxumInfo { - module_import: module.as_str().to_string().replace('/', "_").to_lowercase(), + module_import, axum_route, } } } +// TODO: to be extended with common scenarios +const NO_HTML_EXTENSIONS: [&str; 1] = ["xml"]; + #[derive(Debug, PartialEq, Eq)] pub struct Route { path: String, @@ -73,7 +83,7 @@ impl Route { self.axum_info = Some(AxumInfo::new(self.path.clone())) } - pub fn save_ssg_html(&self, reqwest: &Client) { + pub fn save_ssg_file(&self, reqwest: &Client) { let path = &self.path.replace("index", ""); let mut response = reqwest @@ -81,7 +91,7 @@ impl Route { .send() .unwrap(); - let file_path = self.html_file_path(); + let file_path = self.output_file_path(); let parent_dir = file_path.parent().unwrap(); @@ -124,8 +134,16 @@ impl Route { } } - fn html_file_path(&self) -> PathBuf { + fn output_file_path(&self) -> PathBuf { let cleaned_path = self.path.replace("index", ""); + + if NO_HTML_EXTENSIONS + .iter() + .any(|extension| self.path.ends_with(extension)) + { + return PathBuf::from(format!("out/static{}", cleaned_path)); + } + PathBuf::from(format!("out/static{}/index.html", cleaned_path)) } } @@ -173,6 +191,7 @@ mod tests { let routes = [ ("/index", "out/static/index.html"), ("/documentation", "out/static/documentation/index.html"), + ("/sitemap.xml", "out/static/sitemap.xml"), ( "/documentation/routing", "out/static/documentation/routing/index.html", @@ -182,7 +201,7 @@ mod tests { for (path, html) in routes { let route = Route::new(path.to_string()); - assert_eq!(route.html_file_path(), PathBuf::from(html)) + assert_eq!(route.output_file_path(), PathBuf::from(html)) } } } diff --git a/crates/tuono_lib/src/response.rs b/crates/tuono_lib/src/response.rs index bcd79433..5e43c1ac 100644 --- a/crates/tuono_lib/src/response.rs +++ b/crates/tuono_lib/src/response.rs @@ -1,7 +1,7 @@ use crate::Request; use crate::{ssr::Js, Payload}; -use axum::http::StatusCode; -use axum::response::{Html, IntoResponse, Redirect, Response as AxumResponse}; +use axum::http::{HeaderMap, StatusCode}; +use axum::response::{Html, IntoResponse, Redirect}; use axum::Json; use erased_serde::Serialize; @@ -13,6 +13,8 @@ pub struct Props { pub enum Response { Redirect(String), Props(Props), + // TODO: improve this tuple to support a more generic IntoResponse + Custom((StatusCode, HeaderMap, String)), } #[derive(serde::Serialize)] @@ -67,19 +69,19 @@ impl Props { } impl Response { - pub fn render_to_string(&self, req: Request) -> AxumResponse { + pub fn render_to_string(&self, req: Request) -> impl IntoResponse { match self { Self::Props(Props { data, http_code }) => { let payload = Payload::new(&req, data).client_payload().unwrap(); match Js::render_to_string(Some(&payload)) { - Ok(html) => (*http_code, Html(html)).into_response(), - Err(_) => { - (*http_code, Html("500 Internal server error".to_string())).into_response() - } + Ok(html) => (*http_code, Html(html)), + Err(_) => (*http_code, Html("500 Internal server error".to_string())), } + .into_response() } Self::Redirect(to) => Redirect::permanent(to).into_response(), + Self::Custom(response) => response.clone().into_response(), } } @@ -93,6 +95,9 @@ impl Response { Json(JsonResponse::new_redirect(destination.to_string())), ) .into_response(), + // Custom never needs the "data" response since its scope + // is outside the react domain + Self::Custom(_) => (StatusCode::OK, Json("{}")).into_response(), } } }