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. It runs on two triggers: every push to master, and on a daily schedule. On each run 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]
  schedule:
    - cron: "0 2 * * *"

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 }}

The scheduled trigger

The schedule block runs the workflow daily at 02:00 UTC (04:00 CET). GitHub Actions uses standard cron syntax: 0 2 * * * means “at minute 0 of hour 2, every day.”

This exists to support scheduled post publishing. Hugo excludes posts whose date is in the future — it checks the date at build time, not in advance. By rebuilding the site every morning, any post with draft: false and a date that has now passed will go live automatically, without a manual push.

To schedule a post for a specific date, set draft: false and a future timestamp:

---
title: "My post"
date: 2026-06-01T00:00:00.000Z
draft: false
---

Hugo will exclude it from every build until June 1st. The morning build on that date will include it and deploy it.

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.

draft and date

The draft flag and the date field are independent controls:

  • draft: true — Hugo always skips the post, regardless of date. Use this while writing.
  • draft: false + past date — included in every build. Published immediately on push.
  • draft: false + future date — excluded from builds until that date passes, then published automatically by the daily scheduled build.

Setting a future date is how you schedule a post to go live without a manual push. Hugo checks the date at build time — there is no background scheduler. The daily cron job is what makes the date meaningful.

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.

If you want to go further with Cloudflare — tunnels, identity-aware access, and Zero Trust for home lab services — see SASE for Home Labs and Private Services.