A HTML templating library for reusable components in web applications
rust_html is a tiny templating library that let's you easily create reusable HTML templates and components:
use rust_html::{rhtml, Template};
let card_component = |title: &str| {
rhtml! { r#"
<div class="card">
{title}
</div>
"#}
};
let title = "My Website";
let my_template: Template = rhtml! {r#"
<div class="page">
<h1>{title}</h1>
{card_component("Card A")}
{card_component("Card B")}
{card_component("Card C")}
</div>
"#};
let html_string: String = my_template.into();
println!("{}", html_string);
- Valid HTML syntax is enforced at compile-time
- Runtime rust values are automatically escaped to protect against injection attacks
- You can inject any expression or literal (not just identifiers)
The library is designed for creating reusable components for SSR
(server-side rendering), and is particularly nice in combination with front end libraries
like alpine.js
or htmx
. Unlike some other templating libraries, you can use the
standard HTML syntax directly and keep the templates next to your other Rust code.
Install from crates.io using cargo:
cargo add rust_html
The library has only 5 exported functions/types:
rhtml!
: The main macro for creating templatesTemplate
: represents a reusable HTML templateRender
: trait for implementing reusablestruct
componentsUnescaped
: string wrapper for inserting unescaped valuesTemplateGroup
: wrapper to insert aVec<Template>
Note
The Template
struct itself does not implement the Display
trait.
To print or return the HTML value as a String
you can use String::from(my_template)
or just my_template.into()
where applicable.
The rhtml!
macro accepts a single string literal as input, typically
"my text here"
or r#"my text here"#
which is a bit more convenient for HTML.
The macro returns a Template
struct that ensures injection safety when
reusing template within templates.
Inside the macro string you can inject anything that implements either the
std::fmt::Display
or Render
trait by using brackets {}
.
You can escape brackets inside the HTML by using two of them in a row ({{
or }}
).
use rust_html::{rhtml, Template, TemplateGroup};
/// Reusable card component with a title property
fn card_component(title: &str) -> Template {
rhtml! { r#"<div class="card">{title}</div>"# }
}
/// Reusable card group component that creates N cards
fn card_row_component(n_cards: u32, container_class: &str) -> Template {
// For injecting lists of templates, we can use a TemplateGroup
let cards: TemplateGroup = (0..n_cards)
.map(|card_index| {
let title = format!("Card {}", card_index);
card_component(&title)
})
.collect();
rhtml! { r#"
<div class="{container_class}">
{cards}
</div>
"# }
}
// Server endpoint
fn your_endpoint() -> String {
let page_template: Template = rhtml! { r#"
<div class="page">
{card_row_component(3, "my_card_row")}
</div>
"# };
// Convert the `Template` to `String`
// This is typically only done in the endpoint just before
// returning the full HTML. Make sure you also return a
// `Content-Type` of `text/html` in your response
page_template.into()
}
The your_endpoint
function will return the following HTML:
<div class="page">
<div class="my_card_row">
<div class="card">Card 0</div>
<div class="card">Card 1</div>
<div class="card">Card 2</div>
</div>
</div>
In some cases you might want to include simple logic in your template directly. You can use any valid Rust expression inside the macro:
use rust_html::rhtml;
fn main() {
let age = 32;
let page = rhtml! { r#"
<div>
Bob is {if age >= 18 { "an adult" } else { "not an adult" }}
</div>
"# };
println!("{}", String::from(page));
// Output is '<div>Bob is an adult</div>'
}
To prevent ambiguity you must only use {
and }
for opening/closing scopes
in the injected rust code - not inside literals or comments.
The following is therefore not valid:
rhtml! { r#"{ if true { "}" } else { "" } }"# };
// ^
// Bracket not allowed
You can also use structs as components by implementing the Render
trait.
use rust_html::{rhtml, Render, Template};
#[derive(Clone)]
struct CardComponent {
title: String,
content: String,
}
// Implement rust_html rendering for our component
impl Render for CardComponent {
fn render(&self) -> Template {
rhtml! {r#"
<div class="card">
<h1>{self.title}</h1>
<p>{self.content}</p>
</div>
"#}
}
}
fn main() {
let my_card = CardComponent {
title: "Welcome".to_string(),
content: "This is a card".to_string(),
};
let page = rhtml! {r#"
<div class="page">
{my_card}
</div>
"#};
}
Template input is escaped by default to prevent injection attacks, for instance if
a user were to register with a name that contains a <script>
tag.
The following snippet:
let sketchy_user_input = "<script>alert('hi')</script>";
let page = rhtml! {r#"<div>{sketchy_user_input}</div>"#};
println!("{}", String::from(page));
Generates a string where dangerous characters are escaped:
<div><script>alert('hi')</script></div>
If you need the unescaped value, you can use the Unescaped
wrapper.
Caution
Never use Unescaped
on untrusted user input or if you don't
know what you're doing.
use rust_html::{rhtml, Unescaped};
let sketchy_user_input = "<script>alert('hi')</script>";
let unescaped = Unescaped(sketchy_user_input.to_string());
let page = rhtml! {r#"<div>{unescaped}</div>"#};
println!("{}", String::from(page));
...which results in this string:
<div>
<script>
alert("hi");
</script>
</div>
Integrating with any web framework is trivial - simply convert the
template string to the response type for the given framework.
If you're using Axum you can add the axum
feature to get support
for their IntoResponse
trait.
- maud: rust syntax for HTML
- askama: jinja like templating library
- tera: jinja2 like templating library
- handlebars-rust: handlebars templating language for rust
Look at the AWWY website for more examples.
Run tests:
cargo test -p rust_html_tests
Run doc tests:
cargo test -p rust_html_macros