DEV Community

Njuguna Mureithi
Njuguna Mureithi

Posted on

How to Create a Static Site Generator using Rust in less than 100 LOC

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Now, let's start writing some code:

Define Front Matter Content

#[derive(Debug, Deserialize)]
struct Seo {
    title: String
    //....
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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>
    }
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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),
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

That's it! Now, run cargo run --release and check the dist folder for your generated static site.

cd dist
python3 -m http.server
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode
  • You can learn more about comrak and learn about plugins such as code highlighters.
  • You can learn more about hirola and learn how to write reactive UIs in Rust.

Top comments (2)

Collapse
 
jaycruz profile image
JayCruz

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();
| ----------
layouthas typestd::string::String
44 | 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();

Collapse
 
njugunamureithi profile image
Njuguna Mureithi

I will look into that, meanwhile there is a working example here
github.com/geofmureithi/hirola/blo...
which was the basis of this tutorial.