···173173 }
174174 });
175175176176- let page_assets_options = options.page_assets_options();
176176+ let route_assets_options = options.route_assets_options();
177177178178 info!(target: "build", "Output directory: {}", options.output_dir.display());
179179···229229 let route_start = Instant::now();
230230231231 let content = RouteContent::new(content_sources);
232232- let mut page_assets = RouteAssets::new(&page_assets_options);
232232+ let mut page_assets = RouteAssets::new(&route_assets_options);
233233234234 let params = PageParams::default();
235235 let url = route.url(¶ms);
···260260 }
261261 RouteType::Dynamic => {
262262 let content = RouteContent::new(content_sources);
263263- let mut page_assets = RouteAssets::new(&page_assets_options);
263263+ let mut page_assets = RouteAssets::new(&route_assets_options);
264264265265 let pages = route.get_pages(&mut DynamicRouteContext {
266266 content: &content,
···314314 || !build_pages_styles.is_empty()
315315 || !build_pages_scripts.is_empty()
316316 {
317317- fs::create_dir_all(&page_assets_options.output_assets_dir)?;
317317+ fs::create_dir_all(&route_assets_options.output_assets_dir)?;
318318 }
319319320320 if !build_pages_styles.is_empty() || !build_pages_scripts.is_empty() {
···360360 input: Some(bundler_inputs),
361361 minify: Some(rolldown::RawMinifyOptions::Bool(!is_dev())),
362362 dir: Some(
363363- page_assets_options
363363+ route_assets_options
364364 .output_assets_dir
365365 .to_string_lossy()
366366 .to_string(),
···514514 route: String,
515515) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
516516 match render_result {
517517+ // We've handled errors already at this point, but just in case, handle them again here
518518+ RenderResult::Err(e) => Err(e),
517519 RenderResult::Text(html) => {
518520 let included_styles: Vec<_> = page_assets.included_styles().collect();
519521 let included_scripts: Vec<_> = page_assets.included_scripts().collect();
+1-1
crates/maudit/src/build/options.rs
···5757impl BuildOptions {
5858 /// Returns the fully resolved assets options, with the `output_assets_dir` property resolved to be inside `output_dir`.
5959 /// 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.
6060- pub fn page_assets_options(&self) -> RouteAssetsOptions {
6060+ pub fn route_assets_options(&self) -> RouteAssetsOptions {
6161 RouteAssetsOptions {
6262 assets_dir: self.assets.assets_dir.clone(),
6363 output_assets_dir: self.output_dir.join(&self.assets.assets_dir),
···22#![doc = include_str!("../README.md")]
33//!
44//! <div class="warning">
55-//! You are currently reading Maudit API reference. For a more gentle introduction, please refer to our <a href="https://maudit.dev/docs">documentation</a>.
55+//! You are currently reading Maudit API reference. For a more gentle introduction, please refer to our <a href="https://maudit.org/docs">documentation</a>.
66//! </div>
7788// Modules the end-user will interact directly or indirectly with
+36-15
crates/maudit/src/route.rs
···2424/// pub struct Index;
2525///
2626/// impl Route for Index {
2727-/// fn render(&self, ctx: &mut PageContext) -> RenderResult {
2727+/// fn render(&self, ctx: &mut PageContext) -> impl Into<RenderResult> {
2828/// "<h1>Hello, world!</h1>".into()
2929/// }
3030/// }
···3232pub enum RenderResult {
3333 Text(String),
3434 Raw(Vec<u8>),
3535+ Err(Box<dyn std::error::Error>),
3636+}
3737+3838+impl<T> From<Result<T, Box<dyn std::error::Error>>> for RenderResult
3939+where
4040+ T: Into<RenderResult>,
4141+{
4242+ fn from(val: Result<T, Box<dyn std::error::Error>>) -> Self {
4343+ match val {
4444+ Ok(s) => s.into(),
4545+ Err(e) => RenderResult::Err(e),
4646+ }
4747+ }
4848+}
4949+5050+impl From<RenderResult> for Result<RenderResult, Box<dyn std::error::Error>> {
5151+ fn from(val: RenderResult) -> Self {
5252+ match val {
5353+ RenderResult::Err(e) => Err(e),
5454+ _ => Ok(val),
5555+ }
5656+ }
3557}
36583759impl From<String> for RenderResult {
···197219/// pub struct Index;
198220///
199221/// impl Route for Index {
200200-/// fn render(&self, ctx: &mut PageContext) -> RenderResult {
222222+/// fn render(&self, ctx: &mut PageContext) -> impl Into<RenderResult> {
201223/// let logo = ctx.assets.add_image("logo.png");
202224/// let last_entries = &ctx.content.get_source::<ArticleContent>("articles").entries;
203225/// html! {
···299321/// }
300322///
301323/// impl Route<ArticleParams> for Article {
302302-/// fn render(&self, ctx: &mut PageContext) -> RenderResult {
324324+/// fn render(&self, ctx: &mut PageContext) -> impl Into<RenderResult> {
303325/// let params = ctx.params::<ArticleParams>();
304326/// let articles = ctx.content.get_source::<ArticleContent>("articles");
305327/// let article = articles.get_entry(¶ms.article);
···332354/// pub struct Index;
333355///
334356/// impl Route for Index {
335335-/// fn render(&self, ctx: &mut PageContext) -> RenderResult {
357357+/// fn render(&self, ctx: &mut PageContext) -> impl Into<RenderResult> {
336358/// "<h1>Hello, world!</h1>".into()
337359/// }
338360/// }
339361/// ```
340340-pub trait Route<Params = PageParams, Props = (), T = RenderResult>
362362+pub trait Route<Params = PageParams, Props = ()>
341363where
342364 Params: Into<PageParams>,
343365 Props: 'static,
344344- T: Into<RenderResult>,
345366{
346367 fn pages(&self, _ctx: &mut DynamicRouteContext) -> Pages<Params, Props> {
347368 Vec::new()
348369 }
349349- fn render(&self, ctx: &mut PageContext) -> T;
370370+ fn render(&self, ctx: &mut PageContext) -> impl Into<RenderResult>;
350371}
351372352373/// Raw representation of the parameters passed to a page.
···473494}
474495475496/// Extension trait providing generic convenience methods on an instance of a route
476476-pub trait RouteExt<Params = PageParams, Props = (), T = RenderResult>:
477477- Route<Params, Props, T> + InternalRoute
497497+pub trait RouteExt<Params = PageParams, Props = ()>: Route<Params, Props> + InternalRoute
478498where
479499 Params: Into<PageParams>,
480500 Props: 'static,
481481- T: Into<RenderResult>,
482501{
483502 /// Get the URL for this page with the given parameters
484503 ///
···489508}
490509491510// Blanket implementation for all Page implementors that also implement InternalPage
492492-impl<U, Params, Props, T> RouteExt<Params, Props, T> for U
511511+impl<U, Params, Props> RouteExt<Params, Props> for U
493512where
494494- U: Route<Params, Props, T> + InternalRoute,
513513+ U: Route<Params, Props> + InternalRoute,
495514 Params: Into<PageParams>,
496515 Props: 'static,
497497- T: Into<RenderResult>,
498516{
499517}
500518···502520/// We expose it because [`maudit_macros::route`] implements it for the user behind the scenes.
503521pub trait FullRoute: InternalRoute + Sync + Send {
504522 #[doc(hidden)]
505505- fn render_internal(&self, ctx: &mut PageContext) -> RenderResult;
523523+ fn render_internal(
524524+ &self,
525525+ ctx: &mut PageContext,
526526+ ) -> Result<RenderResult, Box<dyn std::error::Error>>;
506527 #[doc(hidden)]
507528 fn pages_internal(&self, context: &mut DynamicRouteContext) -> PagesResults;
508529···511532 }
512533513534 fn build(&self, ctx: &mut PageContext) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
514514- let result = self.render_internal(ctx);
535535+ let result = self.render_internal(ctx)?;
515536 let bytes = finish_route(result, ctx.assets, self.route_raw())?;
516537517538 Ok(bytes)
···88pub struct Index;
991010impl Route for Index {
1111- fn render(&self, ctx: &mut PageContext) -> RenderResult {
1111+ fn render(&self, ctx: &mut PageContext) -> impl Into<RenderResult> {
1212 let image = ctx.assets.add_image("data/logo.svg");
1313 let script = ctx.assets.add_script("data/some_other_script.js");
1414 let style = ctx
···2727 script src=(script.url().unwrap()) {}
2828 a."text-red-500" href=(link_to_first_dynamic) { "Go to first dynamic page" }
2929 }
3030- .into()
3130 }
3231}
+4-4
examples/library/src/build.rs
···17171818 // Options we'll be passing to RouteAssets instances.
1919 // 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.
2020- let page_assets_options = options.page_assets_options();
2020+ let route_assets_options = options.route_assets_options();
21212222 // Create the assets directory if it doesn't exist.
2323- fs::create_dir_all(&page_assets_options.assets_dir)?;
2323+ fs::create_dir_all(&route_assets_options.assets_dir)?;
24242525 for route in routes {
2626 match route.route_type() {
2727 RouteType::Static => {
2828 // Our page does not include content or assets, but we'll set those up for future use.
2929 let content = RouteContent::new(&content_sources);
3030- let mut page_assets = RouteAssets::new(&page_assets_options);
3030+ let mut page_assets = RouteAssets::new(&route_assets_options);
31313232 // Static and dynamic routes share the same interface for building, but static routes do not require any parameters.
3333 // As such, we can just pass an empty set of parameters (the default for PageParams).
···6161 // Every page of a route may share a reference to the same RouteContent and RouteAssets instance, as it can help with caching.
6262 // 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.
6363 let content = RouteContent::new(&content_sources);
6464- let mut page_assets = RouteAssets::new(&page_assets_options);
6464+ let mut page_assets = RouteAssets::new(&route_assets_options);
65656666 let mut dynamic_ctx = DynamicRouteContext {
6767 content: &content,
···3232}
3333```
34343535-`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.
3636-3735## Building pages
38363937The 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.
···47454846 // Options we'll be passing to RouteAssets instances.
4947 // 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.
5050- let page_assets_options = options.page_assets_options();
4848+ let route_assets_options = options.route_assets_options();
51495250 for route in routes {
5351 match route.route_type() {
5452 RouteType::Static => {
5553 // Our page does not include content or assets, but we'll set those up for future use.
5654 let content = RouteContent::new(&content_sources);
5757- let mut page_assets = RouteAssets::new(&page_assets_options);
5555+ let mut route_assets = RouteAssets::new(&route_assets_options);
58565957 // Static and dynamic routes share the same interface for building, but static routes do not require any parameters.
6058 // As such, we can just pass an empty set of parameters (the default for PageParams).
···62606361 // Every page has a PageContext, which contains information about the current route, as well as access to content and assets.
6462 let url = route.url(¶ms);
6565- let mut ctx = PageContext::from_static_route(&content, &mut page_assets, &url);
6363+ let mut ctx = PageContext::from_static_route(&content, &mut route_assets, &url);
66646765 let content = route.build(&mut ctx)?;
6866···93919492Implementing 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.
95939696-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):
9494+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):
97959896```rs
9999-for asset in page_assets.assets() {
9797+for asset in route_assets.assets() {
10098 fs::copy(asset.path(), asset.build_path())?;
10199}
102100```
···133131134132 // Every page of a route may share a reference to the same RouteContent and RouteAssets instance, as it can help with caching.
135133 // 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.
136136- let mut page_assets = RouteAssets::new(&page_assets_options);
134134+ let mut page_assets = RouteAssets::new(&route_assets_options);
137135 let content = RouteContent::new(&content_sources);
138136139137 let dynamic_ctx = DynamicRouteContext {
+3-3
website/content/docs/quick-start.md
···5454pub struct HelloWorld;
5555```
56565757-Every page must `impl` the `Route` trait, with the required method `render`.
5757+Every route must `impl` the `Route` trait, with the required method `render`.
58585959```rs
6060impl Route for HelloWorld {
6161- fn render(&self, ctx: &mut PageContext) -> RenderResult {
6262- "Hello, world!".into()
6161+ fn render(&self, ctx: &mut PageContext) -> impl Into<RenderResult> {
6262+ "Hello, world!"
6363 }
6464}
6565```
+42-65
website/content/docs/routing.md
···44section: "core-concepts"
55---
6677-## Static Routes
77+## Registering Routes
88+99+Routes must be passed to the `coronate` function in [the entrypoint](/docs/entrypoint) in order to be built.
81099-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`.
1111+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.
10121113```rs
1212-use maudit::route::prelude::*;
1414+use routes::Index;
1515+use maudit::{coronate, routes, BuildOptions, BuildOutput};
13161414-#[route("/hello-world")]
1515-pub struct HelloWorld;
1616-1717-impl Route for HelloWorld {
1818- fn render(&self, ctx: &mut PageContext) -> RenderResult {
1919- RenderResult::Text("Hello, world!".to_string())
2020- }
1717+fn main() -> Result<BuildOutput, Box<dyn std::error::Error>> {
1818+ coronate(
1919+ routes![Index],
2020+ vec![].into(),
2121+ BuildOptions::default()
2222+ )
2123}
2224```
23252424-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.
2626+## Static Routes
25272626-Finally, make sure to [register the page](#registering-routes) in the `coronate` function for it to be built.
2828+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)
27292828-## Ergonomic returns
3030+```rs
3131+use maudit::route::prelude::*;
29323030-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.
3333+#[route("/hello-world")]
3434+pub struct HelloWorld;
31353232-```rs
3333-impl Route<(), (), String> for HelloWorld {
3434- fn render(&self, ctx: &mut PageContext) -> String {
3535- "Hello, world!".to_string()
3636+impl Route for HelloWorld {
3737+ fn render(&self, ctx: &mut PageContext) -> impl Into<RenderResult> {
3838+ "Hello, world!"
3639 }
3740}
3841```
4242+4343+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.
39444045Maudit implements `Into<RenderResult>` for the following types:
41464247- `String`, `Vec<u8>`, `&str`, `&[u8]`
4848+- `Result<T, E> where T: Into<RenderResult> and E: std::error::Error` (see [Handling Errors](#handling-errors) for more information)
4349- [Various templating libraries](/docs/templating/)
5050+5151+Finally, make sure to [register the page](#registering-routes) in the `coronate` function for it to be built.
44524553## Dynamic Routes
46544755Maudit 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`.
48564949-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.
5757+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.
50585151-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.
5959+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.
52605361```rs
5462use maudit::route::prelude::*;
···6270}
63716472impl Route<Params> for Post {
6565- fn render(&self, ctx: &mut PageContext) -> RenderResult {
7373+ fn render(&self, ctx: &mut PageContext) -> impl Into<RenderResult> {
6674 let params = ctx.params::<Params>();
6775 RenderResult::Text(format!("Hello, {}!", params.slug))
6876 }
69777070- fn pages(&self, ctx: &mut DynamicRouteContext) -> Routes<Params> {
7878+ fn pages(&self, ctx: &mut DynamicRouteContext) -> Pages<Params> {
7179 vec![Page::from_params(Params {
7280 slug: "hello-world".to_string(),
7381 })]
···94102 format!("Hello, {}!", slug)
95103 }
961049797- fn pages(&self, ctx: &mut DynamicRouteContext) -> Routes<Params> {
105105+ fn pages(&self, ctx: &mut DynamicRouteContext) -> Pages<Params> {
98106 vec![Page::from_params(Params {
99107 slug: "hello-world".to_string(),
100108 })]
···108116109117## Endpoints
110118111111-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.
119119+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.
112120113121```rs
114122use maudit::route::prelude::*;
···117125pub struct HelloWorldJson;
118126119127impl Route for HelloWorldJson {
120120- fn render(&self, ctx: &mut PageContext) -> RenderResult {
121121- RenderResult::Text(r#"{"message": "Hello, world!"}"#.to_string())
122122- }
123123-}
124124-```
125125-126126-Dynamic routes can also return different types of content. For example, to return a JSON response with the post's content, you could write:
127127-128128-```rs
129129-use maudit::route::prelude::*;
130130-131131-#[route("/api/[slug].json")]
132132-pub struct PostJson;
133133-134134-#[derive(Params, Clone)]
135135-pub struct Params {
136136- pub slug: String,
137137-}
138138-139139-impl Route<Params> for PostJson {
140140- fn pages(&self, ctx: &mut DynamicRouteContext) -> Routes<Params> {
141141- vec![Page::from_params(Params {
142142- slug: "hello-world".to_string()
143143- })]
144144- }
145145-146146- fn render(&self, ctx: &mut PageContext) -> RenderResult {
147147- let params = ctx.params::<Params>();
148148-149149- RenderResult::Text(format!(r#"{{"message": "Hello, {}!"}}"#, params.slug))
128128+ fn render(&self, ctx: &mut PageContext) -> impl Into<RenderResult> {
129129+ r#"{"message": "Hello, world!"}"#
150130 }
151131}
152132```
153133154134Endpoints must also be [registered](#registering-routes) in the `coronate` function in order for them to be built.
155135156156-## Registering Routes
136136+## Handling Errors
157137158158-All kinds of routes must be passed to the `coronate` function in [the entrypoint](/docs/entrypoint) in order to be built.
138138+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.
159139160160-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.
140140+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.
161141162142```rs
163163-use routes::Index;
164164-use maudit::{coronate, routes, BuildOptions, BuildOutput};
143143+impl Route for HelloWorld {
144144+ fn render(&self, ctx: &mut PageContext) -> impl Into<RenderResult> {
145145+ some_operation_that_might_fail()?;
165146166166-fn main() -> Result<BuildOutput, Box<dyn std::error::Error>> {
167167- coronate(
168168- routes![Index],
169169- vec![].into(),
170170- BuildOptions::default()
171171- )
147147+ Ok("Hello, world!")
148148+ }
172149}
173150```
···88pub struct Index;
991010impl Route for Index {
1111- fn render(&self, ctx: &mut PageContext) -> RenderResult {
1111+ fn render(&self, ctx: &mut PageContext) -> impl Into<RenderResult> {
1212 let features = [
1313 ("Performant", "Generate a site with thousands of pages in seconds using minimal resources."),
1414 ("Content", "Bring your content to life with built-in support for Markdown, custom components, syntax highlighting, and more."),