In this blog post, we will walk through the development of a custom static website generator in Rust. This generator takes markdown blog posts as input and produces HTML files for your website. The generator's source code is organized into some modules, and we'll explore them step by step. Here's the main structure of the project:
my_website/
├── src/
│ ├── lib.rs
│ ├── posts.rs
│ └── templates.rs
├── templates/
│ ├── index.html
│ └── post.html
├── blog-posts/
│ └── 2023-10-19-First-Post.md
└── static/
Let's start by creating the cargo project and adding the required dependencies:
The /blog-posts files
The post files will be in this folder, and they are just regular markdown files but with a YAML header on top (constrained by ---). This header isn't rendered in the final result and serves only for parsing some post metadata. Here's an example:
2023-10-19-Static-Blog-Tutorial.md
---
title: "First Blog Post"
description: "An example blog post description"
date: "Oct 19, 2023"
---
~lorem ipsum~
The /templates files
This folder stays at root level and our template engine, Askama, will check it for template files. Askama uses regular .html files and {% %} or {{ }} tags for handling dynamic stuff. My website has only 2 templates - index and post. For the sake of simplicity, I'll only show the important Askama parts for both.
index.html
Blog Posts
{% for post in posts %}
{{post.date}} - {{post.title}}
{{post.description}}
{% endfor %}
post.html
{{ title }}
{{ date }}
{{ description }}
{{ body|markdown }}
The src/templates.rs Module
This module defines the Askama template structs for rendering the blog posts and the index page of the website: IndexTemplate and PostTemplate.
use cratePost;
use Template;
The src/posts.rs Module
This module handles the parsing and processing of blog posts. Let's start by importing all necessary modules and defining a Post struct:
use cratePostTemplate;
use Template;
use fs;
use ;
Now let's add 2 helper functions:
read_posts()Reads the contents of all .md files inside /blog-posts.split_header_and_body()Splits the file contents into header and body parts.
We will now add the load_posts() function, which uses the 2 helper functions to read the files, parse the header and body and return an array of Posts.
Lastly, we will add our main posts function: render_posts(). It iterates through the provided vector of blog post data, converts each post into HTML using a template, and then writes the HTML content to individual files inside /dist/blog.
That's it! The posts.rs module is done and we are now able to handle blog posts. Let's move to the final puzzle piece of the project.
The src/lib.rs Module
This module is the entry point of the application. I will just dump the code and explain later:
use Template;
use ;
use ;
use IndexTemplate;
The main() function runs when the application starts and it calls the load_posts() function to load the blog posts, then generates the HTML files for each post and the index page using the render_posts() and render_index() functions, respectively. It also copies static assets from the static/ directory to the ./dist/static/ directory.
The copy_folder() function recursively copies files and directories from the static/ directory to ./dist/static/.
Wrapping Up
The static generator + blog is now fully working. To build we just need to execute the following command:
All static files are now generated in the dist folder! 🎉
To improve development experience, it's possible to setup a simple server and use cargo-watch to watch for file changes, but this step I will leave it up to you to figure out. (it's available in my repo)