Create a simple static site generator with Rust

This is a simple static site generator written in Rust. It is a simple example of how to use Rust programming to create fast and efficient static site generators. PS: I made this for fun and learning purposes, so don't expect it to be perfect.

Getting started

First, I'll create a new project with Cargo:

cargo new my-site

Then, I'll add the following dependencies to the Cargo.toml file:

[dependencies]
comrak = "0.14"

Project structure

The project structure will be as follows:

📦hello-world
 ┣ 📂content
 ┃ ┣ 📂out
 ┃ ┗ 📂src
 ┃ ┃ ┣ 📜about.md
 ┃ ┃ ┗ 📜home.md
 ┣ 📂src
 ┃ ┗ 📜main.rs
 ┣ 📜.gitignore
 ┣ 📜Cargo.lock
 ┣ 📜Cargo.toml

Writing the code

First, we'll import the libraries we need:

use comrak::markdown_to_html; // Markdown parser
use std::fs; // File system

Then, we'll create a struct to hold the content of the markdown files.

struct File {
    name: String,
    content: String,
}

After that, in the main function, we'll define some variables and constants.

// Constants
let src_dir = String::from("./content/src");
let out_dir = String::from("./content/out");

// The vector of files to be processed
let mut files: Vec<File> = vec![];
let mut files_compiled: Vec<File> = vec![];
let mut index = String::from("<ul>");

We will create a function to read the files in the src directory and add them to the files vector.

fn add_files(files: &mut Vec<File>, path: String) {
    // Read the directory
    fs::read_dir(path).unwrap().for_each(|entry| {
        let entry = entry.unwrap();
        let path = entry.path();
        let name = path.file_name().unwrap().to_str().unwrap().to_string();

        // If the entry is a file, add it to the vector
        if path.is_dir() {
            // .. Ignore directories
        } else {
            let content = fs::read_to_string(path).unwrap();
            files.push(File { name, content });
        }
    });
}

Now, we'll call the add_files function to add the files to the files vector.

add_files(&mut files, src_dir);

Now, If we add #[derive(Debug)] to the File struct, we can print the files to the console.

println!("{:#?}", files);

If did everything correctly, you should see something like this:

[
    File {
        name: "about.md",
        content: "# About\n\nThis is the about page.\n",
    },
    File {
        name: "home.md",
        content: "# Home\n\nThis is the home page.\n",
    },
]

Now, we'll add compiled files to the files_compiled vector.

for file in files {
    let content = markdown_to_html(&file.content, &comrak::ComrakOptions::default());

    files_compiled.push(File {
        name: file.name,
        content,
    });
}

And finally, we'll write the compiled files to the out directory.

files_compiled.iter().for_each(|file| {
    fs::write(
        format!("{}/{}", out_dir, file.name.replace(".md", ".html")),
        &file.content,
    )
    .unwrap();
    println!("Wrote file: {}", file.name);
});

To finish, we'll add the files to the index variable.

for file in &files_compiled {
    index.push_str(&format!(
        "<li><a href=\"{}\">{}</a></li>",
        file.name.replace(".md", ".html"),
        file.name.replace(".md", "")
    ));
}
index.push_str("</ul>");
fs::write(format!("{}/{}", out_dir, "index.html"), &index).unwrap();

Now, if we run the program, the out directory should look like this:

📦out
 ┣ 📜about.html
 ┣ 📜home.html
 ┗ 📜index.html

Improvements

We'll add some messages to the console to make it more user-friendly. The final code should look like this:

use comrak::markdown_to_html;
use std::fs;

#[derive(Debug)]
struct File {
    name: String,
    content: String,
}

fn main() {
    // Constants
    let src_dir = String::from("./content/src");
    let out_dir = String::from("./content/out");

    // The vector of files to be processed
    let mut files: Vec<File> = vec![];
    let mut files_compiled: Vec<File> = vec![];
    let mut index = String::from("<ul>");

    // Delete the output directory
    println!("Deleting old files...");
    fs::remove_dir_all(&out_dir).unwrap_or_else(|_| {
        println!("No old files to delete.");
    });

    // Create the output directory
    fs::create_dir(&out_dir).unwrap();

    // Read the files in the source directory
    println!("Reading files...");
    add_files(&mut files, src_dir);

    // Compile the files
    println!("Compiling files...");

    for file in files {
        let content = markdown_to_html(&file.content, &comrak::ComrakOptions::default());

        files_compiled.push(File {
            name: file.name,
            content,
        });
    }

    // Write the compiled files to the output directory
    println!("Writing files...");
    files_compiled.iter().for_each(|file| {
        fs::write(
            format!("{}/{}", out_dir, file.name.replace(".md", ".html")),
            &file.content,
        )
        .unwrap();

        println!("Wrote file: {}", file.name);
    });

    // Write the index file
    println!("Writing index...");
    for file in &files_compiled {
        index.push_str(&format!(
            "<li><a href=\"{}\">{}</a></li>",
            file.name.replace(".md", ".html"),
            file.name.replace(".md", "")
        ));
    }
    index.push_str("</ul>");
    fs::write(format!("{}/{}", out_dir, "index.html"), &index).unwrap();

    // Done
    println!("Done!");
}

fn add_files(files: &mut Vec<File>, path: String) {
    // Read the directory
    fs::read_dir(path).unwrap().for_each(|entry| {
        let entry = entry.unwrap();
        let path = entry.path();
        let name = path.file_name().unwrap().to_str().unwrap().to_string();

        // If the entry is a file, add it to the vector
        if path.is_dir() {
            // .. Ignore directories
        } else {
            let content = fs::read_to_string(path).unwrap();
            files.push(File { name, content });
        }
    });
}

Conclusion

I made an executable from this and to compile 100 md files it took less than 1ms! In case you need all the source code, get it from here.