I wanted a simple blog: write a Markdown file, push to GitHub, and have it live on my domain within a minute. No CMS, no server to maintain, no monthly bill. Here’s exactly how I built it.

The Stack

  • Hugo — static site generator. Builds the entire site in under 100ms.
  • PaperMod — clean, fast Hugo theme with dark mode, tags, and RSS out of the box.
  • GitHub — source of truth. Every push triggers a deploy.
  • GitHub Actions — builds the site and deploys it on every push to master.
  • Cloudflare Workers + Assets — hosts and serves the static files globally. Free tier is more than enough for a personal blog.
  • Cloudflare DNS — manages the custom domain with automatic SSL.

Total cost: zero.

Repository Structure

blog/
├── .github/
│   └── workflows/
│       └── deploy.yml       # CI/CD pipeline
├── content/
│   ├── archive.md           # archive page
│   └── posts/               # blog posts go here
├── themes/
│   └── PaperMod/            # git submodule
├── wrangler.jsonc           # Cloudflare deployment config
└── hugo.toml                # Hugo config

Hugo Configuration

The hugo.toml at the root of the repo configures the site. Key settings:

baseURL = "https://hicke.se/"
title = "hicke.se"
theme = "PaperMod"
enableRobotsTXT = true

[[menu.main]]
  name = "Archive"
  url = "/archive/"
  weight = 10
[[menu.main]]
  name = "Tags"
  url = "/tags/"
  weight = 20

[params]
  defaultTheme = "auto"
  ShowReadingTime = true
  ShowPostNavLinks = true

PaperMod is added as a git submodule so it tracks upstream fixes:

git submodule add --depth=1 https://github.com/adityatelange/hugo-PaperMod.git themes/PaperMod

Cloudflare Workers Configuration

Cloudflare uses a wrangler.jsonc file to understand what to deploy. For a static site, it’s minimal:

{
  "name": "blog",
  "compatibility_date": "2026-05-01",
  "assets": {
    "directory": "public"
  }
}

The assets.directory tells Wrangler to upload the contents of public/ — the folder Hugo outputs to — as static files.

GitHub Actions: The CI/CD Pipeline

This is the workflow that ties everything together. On every push to master, it checks out the repo (including the PaperMod submodule), builds the site with Hugo, and deploys to Cloudflare.

name: Deploy to Cloudflare

on:
  push:
    branches: [master]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          submodules: true

      - name: Setup Hugo
        uses: peaceiris/actions-hugo@v3
        with:
          hugo-version: "0.147.0"
          extended: true

      - name: Build
        run: hugo

      - name: Deploy
        run: npx wrangler deploy
        env:
          CLOUDFLARE_API_TOKEN: ${{ secrets.CF_API_TOKEN }}
          CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CF_ACCOUNT_ID }}

Two secrets need to be added to the GitHub repository under Settings → Secrets and variables → Actions:

  • CF_API_TOKEN — a Cloudflare API token created from the “Edit Cloudflare Workers” template
  • CF_ACCOUNT_ID — your Cloudflare account ID, found on the Workers & Pages overview page

Custom Domain and SSL

Since hicke.se is already managed by Cloudflare DNS, connecting it takes seconds. In the Cloudflare dashboard under Workers & Pages → blog → Custom domains, I added hicke.se and blog.hicke.se. Cloudflare automatically creates the DNS records and provisions SSL certificates — no configuration needed.

Writing a New Post

New posts live in content/posts/. Create a file, write Markdown, set draft: false, commit, push:

---
title: "My post title"
date: 2026-05-01
draft: false
tags: ["tag"]
---

Content here. Images go in `static/images/` and are referenced as `![alt](/images/photo.jpg)`.

From push to live takes about 30 seconds.

Enabling the Archive Page

PaperMod has a built-in archive layout that lists all posts grouped by year and month. It doesn’t activate automatically — you need to create a content file to mount it at a URL.

Create content/archive.md:

---
title: "Archive"
layout: "archives"
url: "/archive/"
summary: archives
---

The layout: "archives" key is what activates PaperMod’s template. The menu entry in hugo.toml pointing to /archive/ then resolves correctly.

Things I Ran Into

A few issues worth knowing about before you attempt the same setup:

Hugo version pinning matters. PaperMod requires Hugo 0.146.0 or later. The HUGO_VERSION environment variable in your workflow (or CF Pages settings) must be set explicitly — do not rely on the default.

Git submodules need explicit checkout. The GitHub Actions checkout step does not fetch submodules by default. Without submodules: true, the themes/PaperMod directory is empty and the build fails.

Use the “Edit Cloudflare Workers” API token template. Other token types or custom scopes will fail with authentication errors when Wrangler tries to deploy.

npx wrangler deploy auto-detects your config. As long as wrangler.jsonc is present at the root, Wrangler reads it and deploys without any interactive prompts — which is exactly what you want in CI.