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.

Image of Timescale

🚀 pgai Vectorizer: SQLAlchemy and LiteLLM Make Vector Search Simple

We built pgai Vectorizer to simplify embedding management for AI applications—without needing a separate database or complex infrastructure. Since launch, developers have created over 3,000 vectorizers on Timescale Cloud, with many more self-hosted.

Read full post →

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.

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more