How to Create a Static Site Generator using Rust in less than 100 LOC
Tags: rust, markdown, ssg
Published: 2023-08-05 12:18:51

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!

```bash
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:

```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

```rust
#[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

```rust
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.

```rust
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

```rust
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

```rust
enum Layout {
    BlogPost,
    Recipe,
    ...
}
struct SeoFroma {
    title: String,
    layout: Layout
}
```

- You can learn more about [comrak](https://docs.rs/comrak/latest/comrak/) and learn about plugins such as code highlighters.
- You can learn more about [hirola](https://docs.rs/hirola/latest/hirola/) and learn how to write reactive UIs in Rust.