I could have used Medium. I could have used Hashnode or Dev.to. But I’m a cloud engineer, and the way I publish my writing should reflect that.
This blog is a Git repository. Every post is a Markdown file. When I push to main, a GitHub Actions pipeline builds the site with Hugo and deploys it to GitHub Pages. My custom domain is managed through Cloudflare. The whole thing is infrastructure-as-code, version-controlled, and automated.
Here’s how I set it up, and why I made the choices I did.
Why Hugo
Static site generators turn Markdown files into HTML. No databases, no server-side rendering, no WordPress security patches at 2am. Hugo is written in Go, which means it builds fast; my entire site compiles in under a second.
I chose Hugo over alternatives like Jekyll or Gatsby for a few reasons. It has no runtime dependencies, just a single binary. The build times are nearly instant. And the theming system is flexible enough to customise without fighting the framework.
The Theme
I’m using PaperMod as a base, heavily customised with a parchment-inspired design. The aesthetic was intentional; warm cream tones, Playfair Display for headings, Lora for body text, IBM Plex Mono for code. I wanted the blog to feel like a technical journal, not a generic developer portfolio.
The custom CSS lives in assets/css/extended/custom.css, which PaperMod loads automatically. No need to fork the theme…just override what you need.
The CI/CD Pipeline
Every push to main triggers a GitHub Actions workflow that:
- Checks out the repository with submodules (the theme lives in
themes/PaperModas a git submodule) - Installs Hugo 0.158.0 with extended support
- Runs
hugo --minifyto build the production site - Deploys the
public/directory to GitHub Pages
name: Deploy Hugo to GitHub Pages
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: true
fetch-depth: 0
- name: Setup Hugo
uses: peaceiris/actions-hugo@v3
with:
hugo-version: '0.158.0'
extended: true
- name: Build
run: hugo --minify
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: ./public
deploy:
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
steps:
- name: Deploy to GitHub Pages
uses: actions/deploy-pages@v4
The entire deployment takes about 30 seconds from push to live.
Custom Domain & DNS
My domain princessokafor.dev is registered through Cloudflare at a cost of about £10/year. The .dev TLD enforces HTTPS by default, which is one less thing to configure.
The DNS setup points to GitHub Pages via four A records and a CNAME:
- Four A records pointing
@to GitHub’s IP addresses (185.199.108-111.153) - One CNAME pointing
wwwtoprincessokafor.github.io
One thing to note: if you’re using Cloudflare, set the proxy status to DNS only (grey cloud) for all records. GitHub Pages needs to handle SSL certificate provisioning directly, and Cloudflare’s proxy interferes with that.
A static/CNAME file containing princessokafor.dev tells GitHub Pages which domain to serve.
Repository Structure
princessokafor.dev/
├── .github/workflows/deploy.yml
├── assets/css/extended/custom.css
├── config/_default/
│ ├── hugo.toml
│ ├── menus.toml
│ └── params.toml
├── content/
│ └── posts/
│ └── 00-building-this-blog/
│ └── index.md
├── layouts/
│ └── index.html
├── static/
│ ├── CNAME
│ └── favicon.svg
└── themes/PaperMod/
Every post lives in its own directory under content/posts/ as a page bundle. This means each post can have its own images and assets without cluttering a shared folder.
What’s Next
This blog exists to document a larger project: building a hybrid AI inference platform that connects a Mac Mini running local LLM inference to Google Cloud Platform via HA VPN with BGP. The infrastructure is provisioned with Terraform, configured with Ansible, and secured with a zero-trust architecture.
The next post will cover Phase 1: Setting up Ollama on Apple Silicon and running local inference on Mistral 7B.
If you want to see the source code for this blog, it’s public: github.com/Princessx0x0/princessokafor.dev