Skip to Content
Course content

232: Building a Simple Static Site Generator in Rust

Click on the "Edit" button in the top corner of the screen to edit your slide content.

We've spent a lot of time on the theory of ownership and the intricacies of the borrow checker. Now, I want us to actually build something that feels like a real tool. We're going to create a minimal Static Site Generator (SSG). The goal is simple: take a folder of Markdown files, convert them to HTML, and wrap them in a basic template. No fancy frameworks, just raw Rust and one reliable crate for parsing.

Picking our toolset

I'm not going to reinvent the Markdown parser. Writing a spec-compliant CommonMark parser is a rabbit hole I don't want us to fall down today. Instead, we'll use pulldown-cmark. It's the industry standard in the Rust ecosystem because it's an event-based pull parser, meaning it's incredibly fast and doesn't allocate a ton of memory.

# Add this to your Cargo.toml
[dependencies]
pulldown-cmark = "0.10"

Walking the input directory

Our logic needs to look at a folder (let's call it content/), find every .md file, and prepare it for conversion. I like using std::fs::read_dir for this. It gives us an iterator over the entries in the directory, which fits perfectly with Rust's functional leanings.

use std::fs;
use std::path::Path;

fn main() -> std::io::Result<()> {
    let input_dir = "content";
    let output_dir = "dist";

    for entry in fs::read_dir(input_dir)? {
        let entry = entry?;
        let path = entry.path();

        if path.extension().and_then(|s| s.to_str()) == Some("md") {
            println!("Processing {:?}...", path);
            // We'll handle the conversion logic here
        }
    }
    Ok(())
}

Oops, the missing folder

Here is where I usually trip up when I'm rushing a prototype. I started writing the logic to save the output files directly into the dist/ folder. I ran the code, and it immediately panicked with a NotFound error. Why? Because I assumed the dist/ folder already existed. Rust's fs::write won't create the parent directory for you; it just expects it to be there.

I had to step back and add fs::create_dir_all(output_dir)? at the start of the program. It's a small detail, but it's a reminder that when you're doing IO in Rust, you have to be explicit about the state of the filesystem.

Turning Markdown into HTML

Now for the core logic. We read the file to a string, pass it through the pulldown-cmark parser, and push the resulting HTML events into a string. I'm using a simple string template here to wrap the content so it actually looks like a webpage and not just a raw snippet of HTML.

use pulldown_cmark::{Parser, Options, html};
use std::fs;
use std::path::Path;

fn main() -> std::io::Result<()> {
    let input_dir = "content";
    let output_dir = "dist";

    // Fix: Ensure the output directory exists first!
    fs::create_dir_all(output_dir)?;

    for entry in fs::read_dir(input_dir)? {
        let entry = entry?;
        let path = entry.path();

        if path.extension().and_then(|s| s.to_str()) == Some("md") {
            let content = fs::read_to_string(&path)?;
            
            // Set up markdown options (e.g., enabling tables)
            let mut options = Options::empty();
            options.insert(Options::ENABLE_TABLES);
            
            let parser = Parser::new_ext(&content, options);
            
            // We render the parser events into an HTML string
            let mut html_output = String::new();
            html::push_html(&mut html_output, parser);

            // Wrap in a basic HTML shell
            let full_html = format!(
                "<html><body>{}</body></html>",
                html_output
            );

            let file_name = path.file_stem().unwrap().to_str().unwrap();
            let output_path = Path::new(output_dir).join(format!("{}.html", file_name));
            
            fs::write(output_path, full_html)?;
        }
    }
    Ok(())
}

One thing to notice here: I'm using file_stem() to strip the .md extension before adding .html. If I didn't, I'd end up with files named post.md.html, which is just messy. By using join() on the Path object, we ensure that the code works whether you're running this on Windows or Linux.




📋 Practical Task

Implement a Custom CSS Wrapper for the SSG

Your task is to modify the Static Site Generator we just built. Instead of the bare-bones <html><body> wrapper, update the program to include a <head> section containing a <style> tag. Inside that style tag, add a CSS rule that sets the body font to sans-serif and limits the max-width of the content to 800px and centers it on the page. Ensure your final HTML output is well-formed and valid.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.