Rust library to generate static websites
5
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: ergonomic returns

+165 -178
+2 -2
benchmarks/md-benchmark/src/page.rs
··· 20 20 }) 21 21 } 22 22 23 - fn render(&self, ctx: &mut PageContext) -> RenderResult { 23 + fn render(&self, ctx: &mut PageContext) -> impl Into<RenderResult> { 24 24 let params = ctx.params::<Params>(); 25 25 let entry = ctx 26 26 .content 27 27 .get_source::<UntypedMarkdownContent>("articles") 28 28 .get_entry(params.file.as_str()); 29 29 30 - entry.render(ctx).into() 30 + entry.render(ctx) 31 31 } 32 32 }
+3 -2
crates/maudit-macros/src/lib.rs
··· 32 32 } 33 33 34 34 impl maudit::route::FullRoute for #struct_name { 35 - fn render_internal(&self, ctx: &mut maudit::route::PageContext) -> maudit::route::RenderResult { 36 - self.render(ctx).into() 35 + fn render_internal(&self, ctx: &mut maudit::route::PageContext) -> Result<maudit::route::RenderResult, Box<dyn std::error::Error>> { 36 + let result: maudit::route::RenderResult = self.render(ctx).into(); 37 + result.into() 37 38 } 38 39 39 40 fn pages_internal(&self, ctx: &mut maudit::route::DynamicRouteContext) -> Vec<(maudit::route::PageParams, Box<dyn std::any::Any + Send + Sync>, Box<dyn std::any::Any + Send + Sync>)> {
+3 -3
crates/maudit/CHANGELOG.md
··· 77 77 pub struct ImagePage; 78 78 79 79 impl Route for ImagePage { 80 - fn render(&self, ctx: &mut PageContext) -> RenderResult { 80 + fn render(&self, ctx: &mut PageContext) -> impl Into<RenderResult> { 81 81 let image = ctx.assets.add_image_with_options( 82 82 "path/to/image.jpg", 83 83 ImageOptions { ··· 115 115 } 116 116 117 117 impl Route<Params, Props> for Post { 118 - fn render(&self, ctx: &mut PageContext) -> RenderResult { 118 + fn render(&self, ctx: &mut PageContext) -> impl Into<RenderResult> { 119 119 let params = ctx.params::<Params>(); 120 120 let props = ctx.props::<Props>(); 121 121 ··· 125 125 ).into() 126 126 } 127 127 128 - fn pages(&self, ctx: &mut DynamicRouteContext) -> Routes<Params, Props> { 128 + fn pages(&self, ctx: &mut DynamicRouteContext) -> Pages<Params, Props> { 129 129 vec![Page::from_params_and_props( 130 130 Params { 131 131 slug: "hello-world".to_string(),
+1 -1
crates/maudit/src/assets.rs
··· 36 36 impl Default for RouteAssetsOptions { 37 37 fn default() -> Self { 38 38 let default_build_options = BuildOptions::default(); 39 - let page_assets_optiosn = default_build_options.page_assets_options(); 39 + let page_assets_optiosn = default_build_options.route_assets_options(); 40 40 41 41 Self { 42 42 assets_dir: default_build_options.assets.assets_dir,
+4 -4
crates/maudit/src/assets/image.rs
··· 78 78 pub fn new( 79 79 path: PathBuf, 80 80 image_options: Option<ImageOptions>, 81 - page_assets_options: &RouteAssetsOptions, 81 + route_assets_options: &RouteAssetsOptions, 82 82 ) -> Self { 83 83 let hash = calculate_hash( 84 84 &path, ··· 86 86 asset_type: HashAssetType::Image( 87 87 image_options.as_ref().unwrap_or(&ImageOptions::default()), 88 88 ), 89 - hashing_strategy: &page_assets_options.hashing_strategy, 89 + hashing_strategy: &route_assets_options.hashing_strategy, 90 90 }), 91 91 ); 92 92 ··· 103 103 }) 104 104 .as_deref(), 105 105 ); 106 - let build_path = make_final_path(&page_assets_options.output_assets_dir, &filename); 107 - let url = make_final_url(&page_assets_options.assets_dir, &filename); 106 + let build_path = make_final_path(&route_assets_options.output_assets_dir, &filename); 107 + let url = make_final_url(&route_assets_options.assets_dir, &filename); 108 108 109 109 Self { 110 110 path,
+4 -4
crates/maudit/src/assets/script.rs
··· 18 18 } 19 19 20 20 impl Script { 21 - pub fn new(path: PathBuf, included: bool, page_assets_options: &RouteAssetsOptions) -> Self { 21 + pub fn new(path: PathBuf, included: bool, route_assets_options: &RouteAssetsOptions) -> Self { 22 22 let hash = calculate_hash( 23 23 &path, 24 24 Some(&HashConfig { 25 25 asset_type: HashAssetType::Script, 26 - hashing_strategy: &page_assets_options.hashing_strategy, 26 + hashing_strategy: &route_assets_options.hashing_strategy, 27 27 }), 28 28 ); 29 29 30 30 let filename = make_filename(&path, &hash, Some("js")); 31 - let build_path = make_final_path(&page_assets_options.output_assets_dir, &filename); 32 - let url = make_final_url(&page_assets_options.assets_dir, &filename); 31 + let build_path = make_final_path(&route_assets_options.output_assets_dir, &filename); 32 + let url = make_final_url(&route_assets_options.assets_dir, &filename); 33 33 34 34 Self { 35 35 path,
+4 -4
crates/maudit/src/assets/style.rs
··· 28 28 path: PathBuf, 29 29 included: bool, 30 30 style_options: &StyleOptions, 31 - page_assets_options: &RouteAssetsOptions, 31 + route_assets_options: &RouteAssetsOptions, 32 32 ) -> Self { 33 33 let hash = calculate_hash( 34 34 &path, 35 35 Some(&HashConfig { 36 36 asset_type: HashAssetType::Style(style_options), 37 - hashing_strategy: &page_assets_options.hashing_strategy, 37 + hashing_strategy: &route_assets_options.hashing_strategy, 38 38 }), 39 39 ); 40 40 41 41 let filename = make_filename(&path, &hash, Some("css")); 42 - let build_path = make_final_path(&page_assets_options.output_assets_dir, &filename); 43 - let url = make_final_url(&page_assets_options.assets_dir, &filename); 42 + let build_path = make_final_path(&route_assets_options.output_assets_dir, &filename); 43 + let url = make_final_url(&route_assets_options.assets_dir, &filename); 44 44 45 45 Self { 46 46 path,
+7 -5
crates/maudit/src/build.rs
··· 173 173 } 174 174 }); 175 175 176 - let page_assets_options = options.page_assets_options(); 176 + let route_assets_options = options.route_assets_options(); 177 177 178 178 info!(target: "build", "Output directory: {}", options.output_dir.display()); 179 179 ··· 229 229 let route_start = Instant::now(); 230 230 231 231 let content = RouteContent::new(content_sources); 232 - let mut page_assets = RouteAssets::new(&page_assets_options); 232 + let mut page_assets = RouteAssets::new(&route_assets_options); 233 233 234 234 let params = PageParams::default(); 235 235 let url = route.url(&params); ··· 260 260 } 261 261 RouteType::Dynamic => { 262 262 let content = RouteContent::new(content_sources); 263 - let mut page_assets = RouteAssets::new(&page_assets_options); 263 + let mut page_assets = RouteAssets::new(&route_assets_options); 264 264 265 265 let pages = route.get_pages(&mut DynamicRouteContext { 266 266 content: &content, ··· 314 314 || !build_pages_styles.is_empty() 315 315 || !build_pages_scripts.is_empty() 316 316 { 317 - fs::create_dir_all(&page_assets_options.output_assets_dir)?; 317 + fs::create_dir_all(&route_assets_options.output_assets_dir)?; 318 318 } 319 319 320 320 if !build_pages_styles.is_empty() || !build_pages_scripts.is_empty() { ··· 360 360 input: Some(bundler_inputs), 361 361 minify: Some(rolldown::RawMinifyOptions::Bool(!is_dev())), 362 362 dir: Some( 363 - page_assets_options 363 + route_assets_options 364 364 .output_assets_dir 365 365 .to_string_lossy() 366 366 .to_string(), ··· 514 514 route: String, 515 515 ) -> Result<Vec<u8>, Box<dyn std::error::Error>> { 516 516 match render_result { 517 + // We've handled errors already at this point, but just in case, handle them again here 518 + RenderResult::Err(e) => Err(e), 517 519 RenderResult::Text(html) => { 518 520 let included_styles: Vec<_> = page_assets.included_styles().collect(); 519 521 let included_scripts: Vec<_> = page_assets.included_scripts().collect();
+1 -1
crates/maudit/src/build/options.rs
··· 57 57 impl BuildOptions { 58 58 /// Returns the fully resolved assets options, with the `output_assets_dir` property resolved to be inside `output_dir`. 59 59 /// e.g. if `output_dir` is `dist` and `assets.assets_dir` is `_maudit`, `output_assets_dir` will return `dist/_maudit`. The user-entered `assets.assets_dir` is also available and unchanged. 60 - pub fn page_assets_options(&self) -> RouteAssetsOptions { 60 + pub fn route_assets_options(&self) -> RouteAssetsOptions { 61 61 RouteAssetsOptions { 62 62 assets_dir: self.assets.assets_dir.clone(), 63 63 output_assets_dir: self.output_dir.join(&self.assets.assets_dir),
+2 -2
crates/maudit/src/content.rs
··· 129 129 /// } 130 130 /// 131 131 /// impl Route<ArticleParams> for Article { 132 - /// fn render(&self, ctx: &mut PageContext) -> RenderResult { 132 + /// fn render(&self, ctx: &mut PageContext) -> impl Into<RenderResult> { 133 133 /// let params = ctx.params::<ArticleParams>(); 134 134 /// let articles = ctx.content.get_source::<ArticleContent>("articles"); 135 135 /// let article = articles.get_entry(&params.article); ··· 221 221 /// } 222 222 /// 223 223 /// impl Route for Article { 224 - /// fn render(&self, ctx: &mut PageContext) -> RenderResult { 224 + /// fn render(&self, ctx: &mut PageContext) -> impl Into<RenderResult> { 225 225 /// let articles = ctx.content.get_source::<ArticleContent>("articles"); 226 226 /// let article = articles.get_entry("my-article"); // returns a ContentEntry 227 227 /// article.render(ctx).into()
+1 -1
crates/maudit/src/lib.rs
··· 2 2 #![doc = include_str!("../README.md")] 3 3 //! 4 4 //! <div class="warning"> 5 - //! You are currently reading Maudit API reference. For a more gentle introduction, please refer to our <a href="https://maudit.dev/docs">documentation</a>. 5 + //! You are currently reading Maudit API reference. For a more gentle introduction, please refer to our <a href="https://maudit.org/docs">documentation</a>. 6 6 //! </div> 7 7 8 8 // Modules the end-user will interact directly or indirectly with
+36 -15
crates/maudit/src/route.rs
··· 24 24 /// pub struct Index; 25 25 /// 26 26 /// impl Route for Index { 27 - /// fn render(&self, ctx: &mut PageContext) -> RenderResult { 27 + /// fn render(&self, ctx: &mut PageContext) -> impl Into<RenderResult> { 28 28 /// "<h1>Hello, world!</h1>".into() 29 29 /// } 30 30 /// } ··· 32 32 pub enum RenderResult { 33 33 Text(String), 34 34 Raw(Vec<u8>), 35 + Err(Box<dyn std::error::Error>), 36 + } 37 + 38 + impl<T> From<Result<T, Box<dyn std::error::Error>>> for RenderResult 39 + where 40 + T: Into<RenderResult>, 41 + { 42 + fn from(val: Result<T, Box<dyn std::error::Error>>) -> Self { 43 + match val { 44 + Ok(s) => s.into(), 45 + Err(e) => RenderResult::Err(e), 46 + } 47 + } 48 + } 49 + 50 + impl From<RenderResult> for Result<RenderResult, Box<dyn std::error::Error>> { 51 + fn from(val: RenderResult) -> Self { 52 + match val { 53 + RenderResult::Err(e) => Err(e), 54 + _ => Ok(val), 55 + } 56 + } 35 57 } 36 58 37 59 impl From<String> for RenderResult { ··· 197 219 /// pub struct Index; 198 220 /// 199 221 /// impl Route for Index { 200 - /// fn render(&self, ctx: &mut PageContext) -> RenderResult { 222 + /// fn render(&self, ctx: &mut PageContext) -> impl Into<RenderResult> { 201 223 /// let logo = ctx.assets.add_image("logo.png"); 202 224 /// let last_entries = &ctx.content.get_source::<ArticleContent>("articles").entries; 203 225 /// html! { ··· 299 321 /// } 300 322 /// 301 323 /// impl Route<ArticleParams> for Article { 302 - /// fn render(&self, ctx: &mut PageContext) -> RenderResult { 324 + /// fn render(&self, ctx: &mut PageContext) -> impl Into<RenderResult> { 303 325 /// let params = ctx.params::<ArticleParams>(); 304 326 /// let articles = ctx.content.get_source::<ArticleContent>("articles"); 305 327 /// let article = articles.get_entry(&params.article); ··· 332 354 /// pub struct Index; 333 355 /// 334 356 /// impl Route for Index { 335 - /// fn render(&self, ctx: &mut PageContext) -> RenderResult { 357 + /// fn render(&self, ctx: &mut PageContext) -> impl Into<RenderResult> { 336 358 /// "<h1>Hello, world!</h1>".into() 337 359 /// } 338 360 /// } 339 361 /// ``` 340 - pub trait Route<Params = PageParams, Props = (), T = RenderResult> 362 + pub trait Route<Params = PageParams, Props = ()> 341 363 where 342 364 Params: Into<PageParams>, 343 365 Props: 'static, 344 - T: Into<RenderResult>, 345 366 { 346 367 fn pages(&self, _ctx: &mut DynamicRouteContext) -> Pages<Params, Props> { 347 368 Vec::new() 348 369 } 349 - fn render(&self, ctx: &mut PageContext) -> T; 370 + fn render(&self, ctx: &mut PageContext) -> impl Into<RenderResult>; 350 371 } 351 372 352 373 /// Raw representation of the parameters passed to a page. ··· 473 494 } 474 495 475 496 /// Extension trait providing generic convenience methods on an instance of a route 476 - pub trait RouteExt<Params = PageParams, Props = (), T = RenderResult>: 477 - Route<Params, Props, T> + InternalRoute 497 + pub trait RouteExt<Params = PageParams, Props = ()>: Route<Params, Props> + InternalRoute 478 498 where 479 499 Params: Into<PageParams>, 480 500 Props: 'static, 481 - T: Into<RenderResult>, 482 501 { 483 502 /// Get the URL for this page with the given parameters 484 503 /// ··· 489 508 } 490 509 491 510 // Blanket implementation for all Page implementors that also implement InternalPage 492 - impl<U, Params, Props, T> RouteExt<Params, Props, T> for U 511 + impl<U, Params, Props> RouteExt<Params, Props> for U 493 512 where 494 - U: Route<Params, Props, T> + InternalRoute, 513 + U: Route<Params, Props> + InternalRoute, 495 514 Params: Into<PageParams>, 496 515 Props: 'static, 497 - T: Into<RenderResult>, 498 516 { 499 517 } 500 518 ··· 502 520 /// We expose it because [`maudit_macros::route`] implements it for the user behind the scenes. 503 521 pub trait FullRoute: InternalRoute + Sync + Send { 504 522 #[doc(hidden)] 505 - fn render_internal(&self, ctx: &mut PageContext) -> RenderResult; 523 + fn render_internal( 524 + &self, 525 + ctx: &mut PageContext, 526 + ) -> Result<RenderResult, Box<dyn std::error::Error>>; 506 527 #[doc(hidden)] 507 528 fn pages_internal(&self, context: &mut DynamicRouteContext) -> PagesResults; 508 529 ··· 511 532 } 512 533 513 534 fn build(&self, ctx: &mut PageContext) -> Result<Vec<u8>, Box<dyn std::error::Error>> { 514 - let result = self.render_internal(ctx); 535 + let result = self.render_internal(ctx)?; 515 536 let bytes = finish_route(result, ctx.assets, self.route_raw())?; 516 537 517 538 Ok(bytes)
+4 -4
crates/oubli/src/lib.rs
··· 71 71 #[route(stringify!($ident))] 72 72 pub struct Index; 73 73 impl Route for Index { 74 - fn render(&self, ctx: &mut PageContext) -> RenderResult { 75 - blog_index_content::<Entry>(Entry, ctx, $name, stringify!($ident)).into() 74 + fn render(&self, ctx: &mut PageContext) -> impl Into<RenderResult> { 75 + blog_index_content::<Entry>(Entry, ctx, $name, stringify!($ident)) 76 76 } 77 77 } 78 78 79 79 #[route(concat!(stringify!($ident), "/[entry]"))] 80 80 pub struct Entry; 81 81 impl Route<BlogEntryParams> for Entry { 82 - fn render(&self, ctx: &mut PageContext) -> RenderResult { 83 - blog_entry_render(ctx, $name, stringify!($ident)).into() 82 + fn render(&self, ctx: &mut PageContext) -> impl Into<RenderResult> { 83 + blog_entry_render(ctx, $name, stringify!($ident)) 84 84 } 85 85 86 86 fn pages(&self, ctx: &mut DynamicRouteContext) -> Pages<BlogEntryParams> {
+1 -2
examples/basics/src/routes/index.rs
··· 6 6 pub struct Index; 7 7 8 8 impl Route for Index { 9 - fn render(&self, ctx: &mut PageContext) -> RenderResult { 9 + fn render(&self, ctx: &mut PageContext) -> impl Into<RenderResult> { 10 10 let logo = ctx.assets.add_image("images/logo.svg"); 11 11 12 12 layout(html! { 13 13 (logo) 14 14 h1 { "Hello World" } 15 15 }) 16 - .into() 17 16 } 18 17 }
+2 -5
examples/blog/src/routes/article.rs
··· 21 21 }) 22 22 } 23 23 24 - fn render(&self, ctx: &mut PageContext) -> RenderResult { 24 + fn render(&self, ctx: &mut PageContext) -> impl Into<RenderResult> { 25 25 let params = ctx.params::<ArticleParams>(); 26 26 let articles = ctx.content.get_source::<ArticleContent>("articles"); 27 27 let article = articles.get_entry(&params.article); 28 28 29 - let headings = article.data(ctx).get_headings(); 30 - println!("{:?}", headings); 31 - 32 - layout(article.render(ctx)).into() 29 + layout(article.render(ctx)) 33 30 } 34 31 }
+2 -2
examples/blog/src/routes/index.rs
··· 11 11 pub struct Index; 12 12 13 13 impl Route for Index { 14 - fn render(&self, ctx: &mut PageContext) -> RenderResult { 14 + fn render(&self, ctx: &mut PageContext) -> impl Into<RenderResult> { 15 15 let articles = ctx.content.get_source::<ArticleContent>("articles"); 16 16 17 17 let markup = html! { ··· 28 28 } 29 29 .into_string(); 30 30 31 - layout(markup).into() 31 + layout(markup) 32 32 } 33 33 }
+2 -2
examples/empty/src/routes/index.rs
··· 4 4 pub struct Index; 5 5 6 6 impl Route for Index { 7 - fn render(&self, _ctx: &mut PageContext) -> RenderResult { 8 - "Hello, world!".into() 7 + fn render(&self, _: &mut PageContext) -> impl Into<RenderResult> { 8 + "Hello, world!" 9 9 } 10 10 }
+1 -2
examples/image-processing/src/routes/index.rs
··· 6 6 pub struct Index; 7 7 8 8 impl Route for Index { 9 - fn render(&self, ctx: &mut PageContext) -> RenderResult { 9 + fn render(&self, ctx: &mut PageContext) -> impl Into<RenderResult> { 10 10 let logo = ctx.assets.add_image("images/logo.svg"); 11 11 let walrus = ctx.assets.add_image_with_options( 12 12 "images/walrus.jpg", ··· 23 23 h2 { "Here's a 200x200 walrus:" } 24 24 (walrus) 25 25 }) 26 - .into() 27 26 } 28 27 }
+1 -2
examples/kitchen-sink/src/routes/dynamic.rs
··· 17 17 .collect() 18 18 } 19 19 20 - fn render(&self, ctx: &mut PageContext) -> RenderResult { 20 + fn render(&self, ctx: &mut PageContext) -> impl Into<RenderResult> { 21 21 let params = ctx.params::<Params>(); 22 22 let image = ctx.assets.add_image("data/social-card.png"); 23 23 ctx.assets ··· 31 31 (image) 32 32 p { (params.page) } 33 33 } 34 - .into() 35 34 } 36 35 }
+1 -2
examples/kitchen-sink/src/routes/endpoint.rs
··· 4 4 pub struct Endpoint; 5 5 6 6 impl Route for Endpoint { 7 - fn render(&self, ctx: &mut PageContext) -> RenderResult { 7 + fn render(&self, ctx: &mut PageContext) -> impl Into<RenderResult> { 8 8 let image = ctx.assets.add_image("data/logo.svg"); 9 9 let some_script = ctx.assets.add_script("data/script.js"); 10 10 ctx.assets ··· 19 19 image.path.to_string_lossy(), 20 20 some_script.path.to_string_lossy() 21 21 ) 22 - .into() 23 22 } 24 23 }
+1 -2
examples/kitchen-sink/src/routes/index.rs
··· 8 8 pub struct Index; 9 9 10 10 impl Route for Index { 11 - fn render(&self, ctx: &mut PageContext) -> RenderResult { 11 + fn render(&self, ctx: &mut PageContext) -> impl Into<RenderResult> { 12 12 let image = ctx.assets.add_image("data/logo.svg"); 13 13 let script = ctx.assets.add_script("data/some_other_script.js"); 14 14 let style = ctx ··· 27 27 script src=(script.url().unwrap()) {} 28 28 a."text-red-500" href=(link_to_first_dynamic) { "Go to first dynamic page" } 29 29 } 30 - .into() 31 30 } 32 31 }
+4 -4
examples/library/src/build.rs
··· 17 17 18 18 // Options we'll be passing to RouteAssets instances. 19 19 // This value automatically has the paths joined based on the output directory in BuildOptions for us, so we don't have to do it ourselves. 20 - let page_assets_options = options.page_assets_options(); 20 + let route_assets_options = options.route_assets_options(); 21 21 22 22 // Create the assets directory if it doesn't exist. 23 - fs::create_dir_all(&page_assets_options.assets_dir)?; 23 + fs::create_dir_all(&route_assets_options.assets_dir)?; 24 24 25 25 for route in routes { 26 26 match route.route_type() { 27 27 RouteType::Static => { 28 28 // Our page does not include content or assets, but we'll set those up for future use. 29 29 let content = RouteContent::new(&content_sources); 30 - let mut page_assets = RouteAssets::new(&page_assets_options); 30 + let mut page_assets = RouteAssets::new(&route_assets_options); 31 31 32 32 // Static and dynamic routes share the same interface for building, but static routes do not require any parameters. 33 33 // As such, we can just pass an empty set of parameters (the default for PageParams). ··· 61 61 // Every page of a route may share a reference to the same RouteContent and RouteAssets instance, as it can help with caching. 62 62 // However, it is not stricly necessary, and you may want to instead create a new instance of RouteAssets especially if you were to parallelize the building of pages. 63 63 let content = RouteContent::new(&content_sources); 64 - let mut page_assets = RouteAssets::new(&page_assets_options); 64 + let mut page_assets = RouteAssets::new(&route_assets_options); 65 65 66 66 let mut dynamic_ctx = DynamicRouteContext { 67 67 content: &content,
+2 -2
examples/library/src/routes/article.rs
··· 21 21 }) 22 22 } 23 23 24 - fn render(&self, ctx: &mut PageContext) -> RenderResult { 24 + fn render(&self, ctx: &mut PageContext) -> impl Into<RenderResult> { 25 25 let params = ctx.params::<ArticleParams>(); 26 26 let articles = ctx.content.get_source::<ArticleContent>("articles"); 27 27 let article = articles.get_entry(&params.article); ··· 29 29 let headings = article.data(ctx).get_headings(); 30 30 println!("{:?}", headings); 31 31 32 - layout(article.render(ctx)).into() 32 + layout(article.render(ctx)) 33 33 } 34 34 }
+2 -2
examples/library/src/routes/index.rs
··· 11 11 pub struct Index; 12 12 13 13 impl Route for Index { 14 - fn render(&self, ctx: &mut PageContext) -> RenderResult { 14 + fn render(&self, ctx: &mut PageContext) -> impl Into<RenderResult> { 15 15 let articles = ctx.content.get_source::<ArticleContent>("articles"); 16 16 let logo = ctx.assets.add_image("images/logo.svg"); 17 17 ··· 30 30 } 31 31 .into_string(); 32 32 33 - layout(markup).into() 33 + layout(markup) 34 34 } 35 35 }
+1 -2
examples/markdown-components/src/routes.rs
··· 9 9 pub struct IndexPage; 10 10 11 11 impl Route for IndexPage { 12 - fn render(&self, ctx: &mut PageContext) -> RenderResult { 12 + fn render(&self, ctx: &mut PageContext) -> impl Into<RenderResult> { 13 13 let examples = ctx.content.get_source::<ComponentExample>("examples"); 14 14 let example = examples.get_entry("showcase"); 15 15 ··· 32 32 } 33 33 } 34 34 } 35 - .into() 36 35 } 37 36 }
+1 -2
examples/oubli-basics/src/routes/index.rs
··· 6 6 pub struct Index; 7 7 8 8 impl Route for Index { 9 - fn render(&self, ctx: &mut PageContext) -> RenderResult { 9 + fn render(&self, ctx: &mut PageContext) -> impl Into<RenderResult> { 10 10 let logo = ctx.assets.add_image("images/logo.svg"); 11 11 12 12 let archetype_store = ctx ··· 20 20 a href=(archetype.id) { (archetype.data(ctx).title) } 21 21 } 22 22 }) 23 - .into() 24 23 } 25 24 }
+3 -3
website/content/docs/content.md
··· 51 51 pub struct SomeArticlePage; 52 52 53 53 impl Route for SomeArticlePage { 54 - fn render(&self, ctx: &mut PageContext) -> RenderResult { 54 + fn render(&self, ctx: &mut PageContext) -> impl Into<RenderResult> { 55 55 let entry = ctx 56 56 .content 57 57 .get_source::<BlogPost>("source_name") ··· 66 66 } 67 67 68 68 (PreEscaped(entry.render(ctx))) 69 - }.into() 69 + } 70 70 } 71 71 } 72 72 ``` ··· 133 133 pub struct DataPage; 134 134 135 135 impl Route for DataPage { 136 - fn render(&self, ctx: &mut PageContext) -> RenderResult { 136 + fn render(&self, ctx: &mut PageContext) -> impl Into<RenderResult> { 137 137 let entry = ctx 138 138 .content 139 139 .get_source::<MyType>("my_data")
+4 -4
website/content/docs/images.md
··· 21 21 pub struct Blog; 22 22 23 23 impl Route for Blog { 24 - fn render(&self, ctx: &mut PageContext) -> RenderResult { 24 + fn render(&self, ctx: &mut PageContext) -> impl Into<RenderResult> { 25 25 let image = ctx.assets.add_image("logo.png"); 26 26 27 - format!("", image.url).into() 27 + format!("", image.url) 28 28 } 29 29 } 30 30 ``` ··· 52 52 pub struct ImagePage; 53 53 54 54 impl Route for ImagePage { 55 - fn render(&self, ctx: &mut PageContext) -> RenderResult { 55 + fn render(&self, ctx: &mut PageContext) -> impl Into<RenderResult> { 56 56 let image = ctx.assets.add_image_with_options( 57 57 "path/to/image.jpg", 58 58 ImageOptions { ··· 62 62 }, 63 63 )?; 64 64 65 - format!("<img src=\"{}\" alt=\"Processed Image\" />", image.url).into() 65 + format!("<img src=\"{}\" alt=\"Processed Image\" />", image.url) 66 66 } 67 67 } 68 68 ```
+6 -8
website/content/docs/library.md
··· 32 32 } 33 33 ``` 34 34 35 - `Box<dyn std::error::Error>` is typically seen as an anti-pattern in Rust, as it makes it hard to handle specific error types. But, for the sake of simplicity, we'll use it here. 36 - 37 35 ## Building pages 38 36 39 37 The first step in building our own entrypoint is to iterate over the routes and build each page. Routes can either be static (i.e. `/index`) or dynamic (i.e. `/articles/[id]`). For now, we'll only handle static routes. ··· 47 45 48 46 // Options we'll be passing to RouteAssets instances. 49 47 // This value automatically has the paths joined based on the output directory in BuildOptions for us, so we don't have to do it ourselves. 50 - let page_assets_options = options.page_assets_options(); 48 + let route_assets_options = options.route_assets_options(); 51 49 52 50 for route in routes { 53 51 match route.route_type() { 54 52 RouteType::Static => { 55 53 // Our page does not include content or assets, but we'll set those up for future use. 56 54 let content = RouteContent::new(&content_sources); 57 - let mut page_assets = RouteAssets::new(&page_assets_options); 55 + let mut route_assets = RouteAssets::new(&route_assets_options); 58 56 59 57 // Static and dynamic routes share the same interface for building, but static routes do not require any parameters. 60 58 // As such, we can just pass an empty set of parameters (the default for PageParams). ··· 62 60 63 61 // Every page has a PageContext, which contains information about the current route, as well as access to content and assets. 64 62 let url = route.url(&params); 65 - let mut ctx = PageContext::from_static_route(&content, &mut page_assets, &url); 63 + let mut ctx = PageContext::from_static_route(&content, &mut route_assets, &url); 66 64 67 65 let content = route.build(&mut ctx)?; 68 66 ··· 93 91 94 92 Implementing asset processing is a bit outside of the scope of this guide, but we'll at least make sure that assets are working by copying them to the output directory. 95 93 96 - This can be done by iterating over the assets registered in `page_assets` and copying them to their build path after having called `route.build()` (which registers the assets used by the page): 94 + This can be done by iterating over the assets registered in `route_assets` and copying them to their build path after having called `route.build()` (which registers the assets used by the page): 97 95 98 96 ```rs 99 - for asset in page_assets.assets() { 97 + for asset in route_assets.assets() { 100 98 fs::copy(asset.path(), asset.build_path())?; 101 99 } 102 100 ``` ··· 133 131 134 132 // Every page of a route may share a reference to the same RouteContent and RouteAssets instance, as it can help with caching. 135 133 // However, it is not stricly necessary, and you may want to instead create a new instance of RouteAssets especially if you were to parallelize the building of pages. 136 - let mut page_assets = RouteAssets::new(&page_assets_options); 134 + let mut page_assets = RouteAssets::new(&route_assets_options); 137 135 let content = RouteContent::new(&content_sources); 138 136 139 137 let dynamic_ctx = DynamicRouteContext {
+3 -3
website/content/docs/quick-start.md
··· 54 54 pub struct HelloWorld; 55 55 ``` 56 56 57 - Every page must `impl` the `Route` trait, with the required method `render`. 57 + Every route must `impl` the `Route` trait, with the required method `render`. 58 58 59 59 ```rs 60 60 impl Route for HelloWorld { 61 - fn render(&self, ctx: &mut PageContext) -> RenderResult { 62 - "Hello, world!".into() 61 + fn render(&self, ctx: &mut PageContext) -> impl Into<RenderResult> { 62 + "Hello, world!" 63 63 } 64 64 } 65 65 ```
+42 -65
website/content/docs/routing.md
··· 4 4 section: "core-concepts" 5 5 --- 6 6 7 - ## Static Routes 7 + ## Registering Routes 8 + 9 + Routes must be passed to the `coronate` function in [the entrypoint](/docs/entrypoint) in order to be built. 8 10 9 - To create a new page in your Maudit project, create a struct and implement the `Route` trait for it, adding the `#[route]` attribute to the struct definition with the path of the route as an argument. The path can be any Rust expression, as long as it returns a `String`. 11 + The first argument to the `coronate` function is a `Vec` of all the routes that should be built. This list can be created using the `routes!` macro to make it more concise. 10 12 11 13 ```rs 12 - use maudit::route::prelude::*; 14 + use routes::Index; 15 + use maudit::{coronate, routes, BuildOptions, BuildOutput}; 13 16 14 - #[route("/hello-world")] 15 - pub struct HelloWorld; 16 - 17 - impl Route for HelloWorld { 18 - fn render(&self, ctx: &mut PageContext) -> RenderResult { 19 - RenderResult::Text("Hello, world!".to_string()) 20 - } 17 + fn main() -> Result<BuildOutput, Box<dyn std::error::Error>> { 18 + coronate( 19 + routes![Index], 20 + vec![].into(), 21 + BuildOptions::default() 22 + ) 21 23 } 22 24 ``` 23 25 24 - The `Route` trait requires the implementation of a `render` method that returns a `RenderResult`. This method is called when the page is built and should return the content that will be displayed. In most cases, you'll be using a templating library to create HTML content. 26 + ## Static Routes 25 27 26 - Finally, make sure to [register the page](#registering-routes) in the `coronate` function for it to be built. 28 + To create a new page in your Maudit project, create a struct and implement the `Route` trait for it, adding the `#[route]` attribute to the struct definition with the path of the route as an argument. The path can be any Rust expression, as long as its value can be converted to String. (i.e. `.to_string()` will be called on it) 27 29 28 - ## Ergonomic returns 30 + ```rs 31 + use maudit::route::prelude::*; 29 32 30 - The `Route` trait accepts a generic parameter in third position for the return type of the `render` method. This type must implement `Into<RenderResult>`, enabling more ergonomic returns in certain cases. 33 + #[route("/hello-world")] 34 + pub struct HelloWorld; 31 35 32 - ```rs 33 - impl Route<(), (), String> for HelloWorld { 34 - fn render(&self, ctx: &mut PageContext) -> String { 35 - "Hello, world!".to_string() 36 + impl Route for HelloWorld { 37 + fn render(&self, ctx: &mut PageContext) -> impl Into<RenderResult> { 38 + "Hello, world!" 36 39 } 37 40 } 38 41 ``` 42 + 43 + The `Route` trait requires the implementation of a `render` method that returns any types that can be converted into `RenderResult`. This method is called when the page is built and should return the content that will be displayed. In most cases, you'll be using a templating library to create HTML content. 39 44 40 45 Maudit implements `Into<RenderResult>` for the following types: 41 46 42 47 - `String`, `Vec<u8>`, `&str`, `&[u8]` 48 + - `Result<T, E> where T: Into<RenderResult> and E: std::error::Error` (see [Handling Errors](#handling-errors) for more information) 43 49 - [Various templating libraries](/docs/templating/) 50 + 51 + Finally, make sure to [register the page](#registering-routes) in the `coronate` function for it to be built. 44 52 45 53 ## Dynamic Routes 46 54 47 55 Maudit supports creating dynamic routes with parameters. Allowing one to create many pages that share the same structure and logic, but with different content. For example, a blog where each post has a unique URL, e.g., `/posts/my-blog-post`. 48 56 49 - To create a dynamic route, export a struct using the `route!` attribute and add parameters by enclosing them in square brackets (ex: `/posts/[slug]`) in the route's path. 57 + To create a dynamic route, export a struct using the `route` attribute and add parameters by enclosing them in square brackets (ex: `/posts/[slug]`) in the route's path. 50 58 51 - In addition to the `render` method, dynamic routes must implement a `routes` method for Page. The `routes` method returns a list of all the possible values for each parameter in the route's path, so that Maudit can generate all the necessary pages. 59 + In addition to the `render` method, dynamic routes must implement a `pages` method for Route. The `pages` method returns a list of all the possible values for each parameter in the route's path, so that Maudit can generate all the necessary pages. 52 60 53 61 ```rs 54 62 use maudit::route::prelude::*; ··· 62 70 } 63 71 64 72 impl Route<Params> for Post { 65 - fn render(&self, ctx: &mut PageContext) -> RenderResult { 73 + fn render(&self, ctx: &mut PageContext) -> impl Into<RenderResult> { 66 74 let params = ctx.params::<Params>(); 67 75 RenderResult::Text(format!("Hello, {}!", params.slug)) 68 76 } 69 77 70 - fn pages(&self, ctx: &mut DynamicRouteContext) -> Routes<Params> { 78 + fn pages(&self, ctx: &mut DynamicRouteContext) -> Pages<Params> { 71 79 vec![Page::from_params(Params { 72 80 slug: "hello-world".to_string(), 73 81 })] ··· 94 102 format!("Hello, {}!", slug) 95 103 } 96 104 97 - fn pages(&self, ctx: &mut DynamicRouteContext) -> Routes<Params> { 105 + fn pages(&self, ctx: &mut DynamicRouteContext) -> Pages<Params> { 98 106 vec![Page::from_params(Params { 99 107 slug: "hello-world".to_string(), 100 108 })] ··· 108 116 109 117 ## Endpoints 110 118 111 - Maudit supports returning other types of content besides HTML, such as JSON, plain text or binary data. To do this, simply add a file extension to the route path and return the content in the `render` method. 119 + Maudit supports returning other types of content besides HTML, such as JSON, plain text or binary data. To do this, simply add a file extension to the route path and return the content in the `render` method. Both static and dynamic routes can be used as endpoints. 112 120 113 121 ```rs 114 122 use maudit::route::prelude::*; ··· 117 125 pub struct HelloWorldJson; 118 126 119 127 impl Route for HelloWorldJson { 120 - fn render(&self, ctx: &mut PageContext) -> RenderResult { 121 - RenderResult::Text(r#"{"message": "Hello, world!"}"#.to_string()) 122 - } 123 - } 124 - ``` 125 - 126 - Dynamic routes can also return different types of content. For example, to return a JSON response with the post's content, you could write: 127 - 128 - ```rs 129 - use maudit::route::prelude::*; 130 - 131 - #[route("/api/[slug].json")] 132 - pub struct PostJson; 133 - 134 - #[derive(Params, Clone)] 135 - pub struct Params { 136 - pub slug: String, 137 - } 138 - 139 - impl Route<Params> for PostJson { 140 - fn pages(&self, ctx: &mut DynamicRouteContext) -> Routes<Params> { 141 - vec![Page::from_params(Params { 142 - slug: "hello-world".to_string() 143 - })] 144 - } 145 - 146 - fn render(&self, ctx: &mut PageContext) -> RenderResult { 147 - let params = ctx.params::<Params>(); 148 - 149 - RenderResult::Text(format!(r#"{{"message": "Hello, {}!"}}"#, params.slug)) 128 + fn render(&self, ctx: &mut PageContext) -> impl Into<RenderResult> { 129 + r#"{"message": "Hello, world!"}"# 150 130 } 151 131 } 152 132 ``` 153 133 154 134 Endpoints must also be [registered](#registering-routes) in the `coronate` function in order for them to be built. 155 135 156 - ## Registering Routes 136 + ## Handling Errors 157 137 158 - All kinds of routes must be passed to the `coronate` function in [the entrypoint](/docs/entrypoint) in order to be built. 138 + Maudit implements `Into<RenderResult>` for `Result<T: Into<RenderResult>, E: std::error::Error>`. This allows you to use the `?` operator in your `render` method to ergonomically propagate errors that may occur during rendering without needing to change the function's signature. 159 139 160 - The first argument to the `coronate` function is a `Vec` of all the routes that should be built. This list can be created using the `routes!` macro to make it more concise. 140 + The error will be propagated all the way to [`coronate()`](https://docs.rs/maudit/latest/maudit/fn.coronate.html), which will return an error if any page fails to render. 161 141 162 142 ```rs 163 - use routes::Index; 164 - use maudit::{coronate, routes, BuildOptions, BuildOutput}; 143 + impl Route for HelloWorld { 144 + fn render(&self, ctx: &mut PageContext) -> impl Into<RenderResult> { 145 + some_operation_that_might_fail()?; 165 146 166 - fn main() -> Result<BuildOutput, Box<dyn std::error::Error>> { 167 - coronate( 168 - routes![Index], 169 - vec![].into(), 170 - BuildOptions::default() 171 - ) 147 + Ok("Hello, world!") 148 + } 172 149 } 173 150 ```
+2 -2
website/content/news/maudit01.md
··· 34 34 pub struct Index; 35 35 36 36 impl Route for Index { 37 - fn render(&self, ctx: &mut PageContext) -> RenderResult { 38 - "Hello World".into() 37 + fn render(&self, ctx: &mut PageContext) -> impl Into<RenderResult> { 38 + "Hello World" 39 39 } 40 40 } 41 41 ```
+3 -4
website/src/layout.rs
··· 8 8 use maudit::assets::StyleOptions; 9 9 use maudit::content::MarkdownHeading; 10 10 use maudit::maud::generator; 11 - use maudit::route::{RenderResult, PageContext}; 11 + use maudit::route::{PageContext, RenderResult}; 12 12 13 13 pub fn docs_layout( 14 14 main: Markup, 15 15 ctx: &mut PageContext, 16 16 headings: &[MarkdownHeading], 17 - ) -> RenderResult { 17 + ) -> impl Into<RenderResult> { 18 18 layout( 19 19 html! { 20 20 div.container.mx-auto."grid-cols-(--docs-columns)".grid."min-h-[calc(100%-64px)]" { ··· 40 40 bottom_border: bool, 41 41 licenses: bool, 42 42 ctx: &mut PageContext, 43 - ) -> RenderResult { 43 + ) -> impl Into<RenderResult> { 44 44 ctx.assets 45 45 .include_style_with_options("assets/prin.css", StyleOptions { tailwind: true }); 46 46 ··· 91 91 } 92 92 } 93 93 } 94 - .into() 95 94 }
+1 -1
website/src/routes/404.rs
··· 7 7 pub struct NotFound; 8 8 9 9 impl Route for NotFound { 10 - fn render(&self, ctx: &mut PageContext) -> RenderResult { 10 + fn render(&self, ctx: &mut PageContext) -> impl Into<RenderResult> { 11 11 layout( 12 12 html! { 13 13 div.container.mx-auto.text-center.my-50.flex.items-center.flex-col."gap-y-4"."px-8"."sm:px-0" {
+2 -3
website/src/routes/chat.rs
··· 1 1 use maud::html; 2 2 use maudit::route::prelude::*; 3 3 4 - #[route("/chat")] 4 + #[route("/chat/")] 5 5 pub struct ChatRedirect; 6 6 7 7 pub const DISCORD_INVITE: &str = "https://discord.gg/84pd4QtmzA"; 8 8 9 9 impl Route for ChatRedirect { 10 - fn render(&self, _: &mut PageContext) -> RenderResult { 10 + fn render(&self, _: &mut PageContext) -> impl Into<RenderResult> { 11 11 html! { 12 12 head { 13 13 meta http-equiv="refresh" content=(format!("0;url={}", DISCORD_INVITE)); 14 14 } 15 15 } 16 - .into() 17 16 } 18 17 }
+1 -1
website/src/routes/contribute.rs
··· 7 7 pub struct Contribute; 8 8 9 9 impl Route for Contribute { 10 - fn render(&self, ctx: &mut PageContext) -> RenderResult { 10 + fn render(&self, ctx: &mut PageContext) -> impl Into<RenderResult> { 11 11 layout( 12 12 html!( 13 13 div.container.w-full.max-w-larger-prose.mx-auto.my-14.flex.flex-col."gap-y-12"."px-8"."sm:px-0" {
+2 -2
website/src/routes/docs.rs
··· 7 7 pub struct DocsIndex; 8 8 9 9 impl Route for DocsIndex { 10 - fn render(&self, ctx: &mut PageContext) -> RenderResult { 10 + fn render(&self, ctx: &mut PageContext) -> impl Into<RenderResult> { 11 11 let index_page = ctx 12 12 .content 13 13 .get_source::<DocsContent>("docs") ··· 55 55 }) 56 56 } 57 57 58 - fn render(&self, ctx: &mut PageContext) -> RenderResult { 58 + fn render(&self, ctx: &mut PageContext) -> impl Into<RenderResult> { 59 59 let slug = ctx.params::<DocsPageParams>().slug.clone(); 60 60 let entry = ctx 61 61 .content
+1 -1
website/src/routes/index.rs
··· 8 8 pub struct Index; 9 9 10 10 impl Route for Index { 11 - fn render(&self, ctx: &mut PageContext) -> RenderResult { 11 + fn render(&self, ctx: &mut PageContext) -> impl Into<RenderResult> { 12 12 let features = [ 13 13 ("Performant", "Generate a site with thousands of pages in seconds using minimal resources."), 14 14 ("Content", "Bring your content to life with built-in support for Markdown, custom components, syntax highlighting, and more."),
+2 -2
website/src/routes/news.rs
··· 10 10 pub struct NewsIndex; 11 11 12 12 impl Route for NewsIndex { 13 - fn render(&self, ctx: &mut PageContext) -> RenderResult { 13 + fn render(&self, ctx: &mut PageContext) -> impl Into<RenderResult> { 14 14 let content = ctx.content.get_source::<NewsContent>("news"); 15 15 16 16 // Group articles by year ··· 97 97 }) 98 98 } 99 99 100 - fn render(&self, ctx: &mut PageContext) -> RenderResult { 100 + fn render(&self, ctx: &mut PageContext) -> impl Into<RenderResult> { 101 101 let slug = ctx.params::<NewsPageParams>().slug.clone(); 102 102 let entry = ctx 103 103 .content