Core Concepts

Routing

How to create pages and routes in Maudit

Static Routes

To create a new page in your Maudit project, create a struct and implement the Page 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.

use maudit::page::prelude::*;

#[route("/hello-world")]
pub struct HelloWorld;

impl Page for HelloWorld {
  fn render(&self, ctx: &mut RouteContext) -> RenderResult {
    RenderResult::Text("Hello, world!".to_string())
  }
}

The Page 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.

Finally, make sure to register the page in the coronate function for it to be built.

Ergonomic returns

The Page 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.

impl Page<(), (), String> for HelloWorld {
  fn render(&self, ctx: &mut RouteContext) -> String {
    "Hello, world!".to_string()
  }
}

Maudit implements Into<RenderResult> for the following types:

Dynamic Routes

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.

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.

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.

use maudit::page::prelude::*;

#[route("/posts/[slug]")]
pub struct Post;

#[derive(Params, Clone)]
pub struct Params {
  pub slug: String,
}

impl Page<Params> for Post {
  fn render(&self, ctx: &mut RouteContext) -> RenderResult {
    let params = ctx.params::<Params>();
    RenderResult::Text(format!("Hello, {}!", params.slug))
  }

  fn routes(&self, ctx: &mut DynamicRouteContext) -> Routes<Params> {
    vec![Route::from_params(Params {
      slug: "hello-world".to_string(),
    })]
  }
}

The route parameters are automatically extracted from the URL and made available through the ctx.params::<T>() method in the RouteContext struct, providing type-safe access to the values.

use maudit::page::prelude::*;

#[route("/posts/[slug]")]
pub struct Post;

#[derive(Params, Clone)]
pub struct Params {
  pub slug: String,
}

impl Page for Post {
  fn render(&self, ctx: &mut RouteContext) -> String {
    let slug = ctx.params::<Params>().slug;
    format!("Hello, {}!", slug)
  }

  fn routes(&self, ctx: &mut DynamicRouteContext) -> Routes<Params> {
    vec![Route::from_params(Params {
      slug: "hello-world".to_string(),
    })]
  }
}

The struct used for the parameters must implement Into<RouteParams>, which can be done automatically by deriving the Params trait. The fields of the struct must implement the Display trait, as they will be converted to strings to be used in the final URLs and file paths.

Like static routes, dynamic routes must be registered in the coronate function in order for them to be built.

Endpoints

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.

use maudit::page::prelude::*;

#[route("/api.json")]
pub struct HelloWorldJson;

impl Page for HelloWorldJson {
  fn render(&self, ctx: &mut RouteContext) -> RenderResult {
    RenderResult::Text(r#"{"message": "Hello, world!"}"#.to_string())
  }
}

Dynamic routes can also return different types of content. For example, to return a JSON response with the post's content, you could write:

use maudit::page::prelude::*;

#[route("/api/[slug].json")]
pub struct PostJson;

#[derive(Params, Clone)]
pub struct Params {
  pub slug: String,
}

impl Page<Params> for PostJson {
  fn routes(&self, ctx: &mut DynamicRouteContext) -> Routes<Params> {
    vec![Route::from_params(Params {
      slug: "hello-world".to_string()
    })]
  }

  fn render(&self, ctx: &mut RouteContext) -> RenderResult {
    let params = ctx.params::<Params>();

    RenderResult::Text(format!(r#"{{"message": "Hello, {}!"}}"#, params.slug))
  }
}

Endpoints must also be registered in the coronate function in order for them to be built.

Registering Routes

All kinds of routes must be passed to the coronate function in the entrypoint in order to be built.

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.

use pages::Index;
use maudit::{coronate, routes, BuildOptions, BuildOutput};

fn main() -> Result<BuildOutput, Box<dyn std::error::Error>> {
    coronate(
      routes![Index],
      vec![].into(),
      BuildOptions::default()
    )
}