Setting Up a Simple Blog - Handmade Static Site Generator
November 04, 2025
You can check the repository here.
The idea to start writing a blog has been in my mind for some time now, until today that I decided to get on with it. And what better way to begin than writing about this same process?
What Am I Looking For?
From the start I knew I wanted something simple, easy to maintain and quick to iterate. One of the major reasons I’m doing this is to structure my thinking when working on any type of project, and for that I need not be distracted by implementation details.
Barebones
Still, I would like to have some formatting, as there were times I would take notes in plain text files for then to never come back to them. So I’m using the next closest thing, Markdown.
Now what I need is to convert this Markdown file into a HTML file.
After looking around through some posts on reddit, I found the pandoc document converter, exactly what I
needed. For any Markdown file I just had to run
pandoc input.md -o index.html.
Pandoc uses an extended version of Markdown which comes in handy, as
it includes support for tables, definition lists, footnotes, citations
and even math. It also supports Metadata Blocks, which allows
including information such as % title,
% author and % date. I will only be using
% title since the tool issues a warning when not using
it.
And there it was, just what I wanted, almost.
Not Stylish, Just Yet
A plain HTML file with formatted text is a lot better than a plain text file, but unfortunately it doesn’t look good on the portfolio.
I need something simple, but still good looking. Luckily you can link
a .css file to the output of pandoc using the
--css argument. The problem is I don’t have much experience
using css, so it is time to look for references.
I really like Fabien Sanglard’s and Steve Losh’s websites. They are minimalistic, nice to look at, and easy to read. I appreciate how you can immediately see all the stuff the authors have been working on or pondering over the last couple of years as soon as you enter. With the help of inspect element, a couple of queries to ChatGPT, and a background from Hero Patterns, I ended up with a style I was happy with.
There was now a need for a nice header: css and Markdown alone wouldn’t suffice. Fortunately, pandoc allows for HTML to be written into the Markdown file, which it then passes to the final output unchanged. I can now define a simple header to include on all the pages and ensure a concise style, but to achieve that I would have to copy and paste the same header everytime I create a new page. It would be nice to have some sort of page template.
The Page Template
I went on and created create-post.py, a Python script
that takes <file-name> and
<post-title> as arguments. This script creates
<file-name>.md and writes to it the metadata block
% <post-title>, the page header and the post header
with the date of when the post was created.1
header_date = datetime.now().strftime("%B {S}, %Y").replace('{S}', str(datetime.now().day))
header = f"""%{post_title}
<header>
header content goes here
</header>
## {post_title}
{header_date}
---
"""
with open(f"{posts_path}/{file_name}/{file_name}.md", "w", encoding="utf-8") as f:
f.write(header)
I also included some code to add the post entry along with the date to the home page
home_path = "./home.md"
date_entry = datetime.now().strftime("%d/%m/%Y")
post_entry = f"{date_entry}: [**{post_title}**]({posts_path}/{file_name}/index.html) \n"
with open(home_path, "r", encoding="utf-8") as f:
lines = f.readlines()
lines.insert(7, post_entry) # hardcoded position, for now
with open(home_path, "w", encoding="utf-8") as f:
f.writelines(lines)
Some of this code is hardcoded. I plan on adding config files in the future. To see the full code visit the repository.
With this, I now have an easy way of creating new entries.
Generating the Site
Calling pandoc for every .md file is not ideal. That’s
why I implemented build.py, a minimal build system for
transforming recently modified Markdown files into HTML files.
import os
import subprocess
from pathlib import Path
css_path = Path("style.css").resolve() # absolute path to CSS
ignored_mds = [Path("./README.md")] # will not apply to ALL Markdown files
markdown_files = [md for md in Path(".").rglob("*.md") if md not in ignored_mds]
paired_files = [(md, md.parent / "index.html") for md in markdown_files if md not in ignored_mds] # target: index.html file in the same directory
print("### BUILD ###")
for md, html in paired_files:
mod_time_md = md.stat().st_mtime
if html.exists():
mod_time_html = html.stat().st_mtime
if mod_time_html > mod_time_md:
continue
relative_path_css = os.path.relpath(css_path, start=html.parent) # relative to html and md path
subprocess.run([
"pandoc",
"-s", str(md),
"-o", str(html),
"--css", relative_path_css,
"-V", "title="
])
print(md, "->", html)
-s inserts the necessary headers and footers to create a
full HTML file.
-V title= prevents pandoc of inserting the variable
defined in % title as a header, while still keeping it as
the document title.
Workflow
I will be using vim as my text editor, primarily for three reasons.
- Fast and comfortable to write in.
- Very customizable.
- Looks cool.
Probably one of the most important aspects of using vim in this case is having the option to execute a command when saving the file. Thanks to this, I can now avoid having to call pandoc with the same arguments everytime I want to see the results on the browser. I just save the file and the HTML file is automatically generated.
I added the following to the .vimrc
let s:script_dir = expand('<sfile>:p:h')
autocmd FileType markdown autocmd BufWritePost <buffer> execute '!python3 ' . shellescape(s:script_dir . '/build.py')
This will apply only when saving .md files.2
How about deployment? As I’m using Github Pages for hosting, pushing my local files to the remote repository will deploy the page. The thing is, I don’t want to deploy everytime I correct a minor mistake, it would make version control really uncomfortable.
To fix this I created a new working branch. Every change
I make gets pushed to that branch. And once I feel it’s time to deploy,
I merge into the master branch.
For easy deployment I made a simple shell script
deploy.sh.
set -e
working_branch="working"
git checkout master
git merge "$working_branch" --no-ff -m "Merge $working_branch branch into master"
git push origin master
echo "Master branch updated"
git checkout "$working_branch"
set -e tells the shell to exit immediately if any
command fails.
--no-ff ensures git creates a merge commit even if a
fast-forward is possible.
And there it is, a simple framework for my use case. Every time I
want to write about a new topic, I run create-post.py and
start writing right away. Once I’m done, I simply save and check the
browser. If I’m happy with the result, I commit, push to origin and then
run deploy.sh. And just like that a new entry is added to
the blog.
Initially, I wasn’t familiar with the concept of static site generators. I’ve seen recommendations of tools like Jekyll or Hugo for easily creating personal websites, but I felt they were more than what I needed at the moment3. I also liked the idea of creating a basic blog framework. What I ended up with was a custom static site generator.
Now it’s a matter of time to see how well this framework holds up for me.