2025-10-15
What is creeping around these cursed lands
How dusty are these shelves! And yet, new books have found themselves nestled among the ancient tomes, ready to be discovered by those brave enough to explore these cursed lands.
Last summer, we were very happy to introduce Maudit to you, a Rust library to generate static websites, then at version 0.1, but already quite mighty. This time we're back to talk about what we've been up to recently and what's coming this year.
Interested in trying out Maudit? Follow our Quick Start guide.
Maudit 0.4.0 added support for image processing, allowing you to easily resize, convert, and optimize images for your website at build time.
use maud::html;
use maudit::route::prelude::*;
#[route("/image")]
pub struct ImagePage;
impl Route for ImagePage {
fn render(&self, ctx: &mut PageContext) -> impl Into<RenderResult> {
let image = ctx.assets.add_image_with_options(
"path/to/image.jpg",
ImageOptions {
width: Some(800),
height: None,
format: Some(ImageFormat::Png),
},
)?;
Ok(html! {
(image.render("My 800 pixel wide PNG"))
})
}
}
See our section on image processing for more information on how to use images in Maudit.
Maudit also includes the ability to easily create low-quality image placeholders (LQIP) for your images using ThumbHash.
use maudit::route::prelude::*;
#[route("/image")]
pub struct ImagePage;
impl Route for ImagePage {
fn render(&self, ctx: &mut PageContext) -> impl Into<RenderResult> {
let image = ctx.assets.add_image("path/to/image.jpg")?;
let placeholder = image.placeholder()?;
Ok(format!("<img src=\"{}\" alt=\"Image with placeholder\" style=\"background-image: url('{}'); background-size: cover;\" />", image.url(), placeholder.data_uri()))
}
}
Check our documentation on placeholders for more information.
Maudit 0.5.0 added support for components and shortcodes in Markdown files. These features allow you to completely customize how your Markdown files are rendered and enhance them with new possibilities.
Embedding a YouTube video typically means copying a long, ugly iframe tag and configuring several attributes to ensure proper rendering. It'd be nice to have something friendlier, a code that would be short, if you will.
Here's my cool video:
{{ youtube id="b_KfnGBtVeA" /}}
content_sources![
"articles" => glob_markdown_with_options::<ArticleContent>("content/articles/*.md", MarkdownOptions {
shortcodes: {
let mut shortcodes = MarkdownShortcodes::default();
shortcodes.register("youtube", |attrs, _| {
if let Some(id) = attrs.get::<String>("id") {
format!(r#"<iframe width="560" height="315" src="https://www.youtube.com/embed/{}" frameborder="0" allowfullscreen></iframe>"#, id)
} else {
panic!("YouTube shortcode requires an 'id' attribute");
}
});
shortcodes
},
..Default::default()
})
],
For more information, read our section on shortcodes.
Sometimes, you want to keep writing normal, spec-compliant Markdown, but still be able to add a bit of spice to it. For this, Maudit supports components, allowing you to use custom code when rendering normal Markdown elements.
For instance, you may want to add an anchor icon to every heading, without needing to use a {{ heading }} shortcode.
use maudit::components::MarkdownComponents;
struct CustomHeading;
impl HeadingComponent for CustomHeading {
fn render_start(&self, level: u8, id: Option<&str>, _classes: &[&str]) -> String {
let id_attr = id.map(|i| format!(" id=\"{}\"", i)).unwrap_or_default();
let href = id.map(|i| format!("#{}", i)).unwrap_or_default();
format!(
"<div><a href=\"{href}\"><span aria-hidden=\"true\">{}</span></a><h{level}{id_attr}>", include_str("icons/anchor.svg")
)
}
fn render_end(&self, level: u8) -> String {
format!("</h{level}></div>")
}
}
content_sources![
"blog" => glob_markdown_with_options::<BlogPost>("content/blog/**/*.md", MarkdownOptions {
components: MarkdownComponents::new().heading(CustomHeading),
..Default::default()
}),
],
For more information, read our section on components.
Maudit 0.6.0 and 0.6.6 made it much easier to handle errors within pages by making all asset methods (which are prone to filesystem errors) return Result instead of panicking.
Additionally, pages themselves can now optionally return Result and will bubble their errors up the chain to the entrypoint when using the ? operator. Maudit implements Into<RenderResult> for Result<T: Into<RenderResult>, E: Error>, and as such, using ? and returning Result requires no signature changes inside your pages.
use maudit::route::prelude::*;
use maud::html;
#[route("/example")]
pub struct Example;
impl Route for Example {
fn render(&self, ctx: &mut PageContext) -> impl Into<RenderResult> {
// Use the ? operator to bubble up asset-related errors
let logo = ctx.assets.add_image("images/logo.png")?;
// Wrap your return value with Ok()
Ok(html! {
(logo)
p { "My cool logo!" }
})
}
}
Or you can just unwrap() everything, that's ok! Check our section on handling errors if you'd like to learn more.
Maudit 0.7.0 added support for internationalizing routes. For instance, you may want to have /about in English, but /a-propos and /om-oss in French and Swedish respectively.
This is already possible in Maudit: You can duplicate your About struct twice, register the two new routes, rewrite the render implementation twice… but that's a bit cumbersome, so Maudit now allows you to generate all these pages using a single struct:
use maudit::route::prelude::*;
#[route(
"/contact",
locales(sv(prefix = "/sv"), de(path = "/de/kontakt"))
)]
pub struct Contact;
impl Route for Contact {
fn render(&self, ctx: &mut PageContext) -> impl Into<RenderResult> {
match &ctx.variant {
Some(language) => match language.as_str() {
"sv" => "Kontakta oss.",
"de" => "Kontaktieren Sie uns.",
_ => unreachable!(),
},
_ => "Contact us.",
}
}
}
The ergonomics are still a bit iffy, but this nonetheless already makes it much easier to localize your website. To learn more about internationalization, visit our documentation.
Maudit 0.9.0 added support for automatically generating a sitemap for your website. In this new world of AI and other advanced web crawlers, sitemaps are a bit of an old relic. However, they're still considered useful to ensure that search engines properly index your website.
To make Maudit generate a sitemap, first configure the base_url property on BuildOptions to your website's address and then enable sitemaps by setting sitemap with a SitemapOptions struct with enabled: true.
use maudit::{BuildOptions, SitemapOptions, content_sources, coronate, routes};
fn main() {
coronate(
routes![],
content_sources![],
BuildOptions {
base_url: Some("https://example.com".into()),
sitemap: SitemapOptions {
enabled: true,
..Default::default()
},
..Default::default()
},
);
}
With this, building your website will now result in a sitemap.xml file being generated inside your dist folder, which includes all the pages of your website. Maudit will also automatically handle separating your sitemap into multiple files if you have over the recommended maximum of 50,000 pages per sitemap.
For more information on sitemap generation in Maudit, check our sitemap documentation.
A common complaint about MPAs (Multi-Page Applications) is that navigating between pages is slow, especially compared to the app-like experience of SPAs (Single-Page Applications).
The solution to this problem is to prefetch pages before the user navigates to them, like SPAs typically do, allowing near-instant navigations in most cases.
Since Maudit 0.10.0, Maudit will by default prefetch links on mousedown, improving page loads by around 80 ms on average, with other prefetching strategies available such as prefetching on hover.
Showing the Hover strategy for prefetchingFor more information on prefetching, see our prefetching documentation.
Maudit 0.10.0 also added a new redirect() function to... well, redirect to another page.
use maudit::route::prelude::*;
#[route("/redirect")]
pub struct Redirect;
impl Route for Redirect {
fn render(&self, ctx: &mut PageContext) -> impl Into<RenderResult> {
redirect("https://example.com")
// Use a page's url method to generate type-safe links:
// redirect(&OtherPage.url(None))
}
}
Simple enough. The return value of this function can be directly used in your pages, making it nice and easy to redirect to new content. To learn more about redirects, redirect yourself to our documentation.
Maudit is mightier than before, but there are still so many twisted paths we'd like to follow. Including, but not limited to:
For now, we go back into hiding. See you soon!