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.
Comments (0)