In this tutorial, we will learn how to create a static site generator using Rust. We will use the following tools:
- Rust (of course)
- Markdown (Our content will be in markdown)
- FrontMatter (Markdown content will contain YAML metadata)
Let's get started!
cargo new samosa-site
Create a new folder named pages inside samosa-site and move the following files into it:
- index.md (The root of our site)
- recipes/samosa-waru.md
- recipes/samosa-beef.md
In the future, if we add more recipes, we expect the code to keep working.
Let's import some libraries in our Cargo.toml:
hirola = { version ="0.3", default-features = false, features = ["ssr"] } # For rendering html to string
glob = "0.3.1" # For searching through directories
comrak = { version = "0.18" } # For parsing markdown
fronma = "0.2" # For parsing front matter
serde = { version = "1", features = ["derive"] } # Required by fronma
Now, let's start writing some code:
Define Front Matter Content
#[derive(Debug, Deserialize)]
struct Seo {
title: String
//....
}
This would be read from markdown looking like this:
---
title: "A recipe for Beef samosas cooked Kenyan style"
---
# Kenyan style Beef samosas recipe
.....(more content here)
That done, let's now handle the layout for our site.
Here is a basic example but you should be able to write complex layouts
fn layout(seo: Seo) -> Dom {
html! {
<html>
<head>
<title>{&seo.title} " | Awesome Samosa Site"</title>
<meta charset="utf-8"/>
<meta property="og:title" content={&seo.title}/>
</head>
<body>
// markdown content will go here
"__MARKDOWN_CONTENT_HERE__"
<body>
</html>
}
}
Reading Markdown Files
We will open a file, parse it, and return the HTML and Seo content in a tuple.
fn read_page(path: &PathBuf) -> (String, Seo) {
use comrak::{markdown_to_html, ComrakOptions};
let markdown = std::fs::read_to_string(path).unwrap(); // Open the path
let data = fronma::parser::parse::<Seo>(&markdown)
.expect(&format!("in file: {}", path.to_string_lossy())); // Parse front matter and markdown
let res = markdown_to_html(&data.body, &ComrakOptions::default()); // convert markdown to html
(res, data.headers)
}
Bringing it all together
fn main() {
use glob::glob;
for entry in glob("src/pages/**/*.md").expect("Failed to read glob pattern") {
match entry {
Ok(path) => {
let (content, seo) = read_page(&path);
let mut layout = "<!DOCTYPE html>".to_string();
layout.extend(render_to_string(layout(seo)).chars());
let html_path = path
.to_string_lossy()
.replace("src/pages", "dist")
.replace(".md", ".html");
std::fs::create_dir_all("dist/recipes").unwrap();
let file = File::create(&html_path).unwrap();
let html_page = layout.replace("__MARKDOWN_CONTENT_HERE__", &content).as_bytes();
file.write_all(html_page).expect("Unable to write data");
}
Err(e) => println!("{:?}", e),
}
}
}
That's it! Now, run cargo run --release
and check the dist folder for your generated static site.
cd dist
python3 -m http.server
What next?
- You can add a layout option in our
Seo
and frontmatter then use that to render different layouts
enum Layout {
BlogPost,
Recipe,
...
}
struct SeoFroma {
title: String,
layout: Layout
}
Top comments (2)
Maybe I'm doing something wrong, but I'm getting an error on the call for layout
let mut layout = "<!DOCTYPE html>".to_string();
layout.extend(render_to_string(layout(seo)).chars());
The error is stating:
let mut layout = "<!DOCTYPE html>".to_string();
layout| ----------
has type
std::string::String44 | layout.extend(render_to_string(layout(seo)).chars());
| ^^^^^^-----
| |
| call expression requires function
I tried renaming the the function called layout to Layout, but then I get a "temporary value dropped while borrowed" on this line:
let html_page = layout.replace("__MARKDOWN_CONTENT_HERE__", &content).as_bytes();
I will look into that, meanwhile there is a working example here
github.com/geofmureithi/hirola/blo...
which was the basis of this tutorial.