Rust
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Ownership and Borrowing
-
Section 4: Structuring Data
-
Section 5: Collections and Error Handling
-
Section 6: Traits and Generics
-
Section 7: Concurrency
-
Section 8: Building for the Web
-
Section 9: Memory and Performance
-
Section 10: More Standard Library and Ecosystem
-
Section 11: Advanced Rust
-
Section 12: Rust for Systems and WebAssembly
-
Section 13: Tooling and Best Practices
-
Section 14: Data Structures and Algorithms in Rust
-
Section 15: Practical Projects
-
Section 16: Interview Practice
-
Section 17: std::collections In Depth
-
Section 18: std::io and std::fs In Depth
-
Section 19: std::net
-
Section 20: std::option and std::result In Depth
-
Section 21: std::iter In Depth
-
Section 22: std::sync In Depth
-
Section 23: std::string and std::str
-
Section 24: Cargo and Crates.io Ecosystem
-
Section 25: Popular Crates Ecosystem
-
Section 26: Rust Memory Model Deep Dive
-
Section 27: More Practice Exercises
-
Section 28: More Interview Practice
-
Section 29: Async Rust Deep Dive
-
Section 30: Tokio Ecosystem In Depth
-
Section 31: Error Handling Ecosystem Deep Dive
-
Section 32: Serde In Depth
-
Section 33: Web Development with Rust Deep Dive
-
Section 34: Database Access Ecosystem
-
Section 35: Rust for Embedded Systems Deep Dive
-
Section 36: Rust Macros In Depth
-
Section 37: Advanced Trait System
-
Section 38: Unsafe Rust In Depth
-
Section 39: Rust CLI Development
-
Section 40: Testing Ecosystem Deep Dive
-
Section 41: WebAssembly Deep Dive
-
Section 42: Rust Design Patterns
-
Section 43: More Data Structures in Rust
-
Section 44: Final Practice Projects
-
Section 45: Rust Performance Optimization
-
Section 46: Rust Ecosystem Tooling
-
Section 47: More Interview and Review
232: Building a Simple Static Site Generator in Rust
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.
There are no comments for now.