A Cost-Effective Personal Blog with Hugo, GitHub, and Cloudflare Pages

Build and publish a fast personal blog with Hugo, GitHub, and Cloudflare Pages, with the domain as the only recurring cost.

A personal blog does not need a database, a virtual server, or another monthly subscription. If the site is mostly articles, images, and a few static pages, a static-site stack can be simpler, faster, and almost free to run.

This setup uses Hugo to build the site, GitHub to store its source, and Cloudflare Pages to publish it. The only recurring cost is usually the domain renewal.

The short version: write a post, push it to GitHub, and Cloudflare automatically publishes it.

Why a static blog?

Traditional publishing platforms often bundle together a web server, database, admin panel, plugins, updates, and backups. That makes sense for a dynamic site, but it is a lot of machinery for a personal blog.

Hugo takes a different approach. It converts Markdown files into plain HTML, CSS, JavaScript, and images. The deployed site does not need a database or application server. There is less to maintain, and static files are easy to cache around the world.

The result is pleasantly boring:

  • no server operating system to patch
  • no database to back up
  • no WordPress plugins to babysit
  • no idle virtual machine running all month
  • very little attack surface
  • fast page loads almost everywhere

The stack

Hugo

Hugo is the site generator. Posts live as Markdown files in a Git repository, usually in a structure like this:

content/
  post/
    first-post/
      index.md
      images/
        cover.jpg

A basic site configuration might include:

baseurl = "https://tkalp.dev/"
languageCode = "en-US"
title = "tkalp.dev"
theme = ["your-theme"]
enableRobotsTXT = true

The baseurl value matters more than it first appears. Hugo uses it for canonical URLs, feeds, sitemaps, and other generated links. It should match the final HTTPS domain, including the trailing slash.

GitHub

GitHub is the source of truth. It stores the Hugo configuration, theme, posts, and images while giving every change a useful history.

A normal publishing workflow is simply:

git add .
git commit -m "post: add a new article"
git push origin main

There is no need to upload the generated public directory manually. Cloudflare can build it directly from the source repository.

Cloudflare Pages

Cloudflare Pages connects to the GitHub repository. Every push to the production branch starts a build and, if it succeeds, publishes the result to Cloudflare’s global network.

For this Hugo project, the important build settings are:

Production branch: main
Build command: hugo --gc --minify --baseURL https://tkalp.dev/
Build output directory: public
Environment variable: HUGO_VERSION=0.163.1

Pinning the Hugo version is a small but worthwhile detail. It prevents an unexpected generator update from changing the build or breaking an older theme. Cloudflare’s official Hugo deployment guide covers the repository connection and build settings.

Connect the custom domain

Cloudflare Pages provides a temporary project.pages.dev address, but a personal domain makes the site easier to remember and gives it a permanent identity.

In the Pages project:

  1. Open Custom domains.
  2. Add the apex domain, such as tkalp.dev.
  3. Add www.tkalp.dev if it should also work.
  4. Wait for the DNS record and TLS certificate to become active.

When the domain is already managed by Cloudflare, most of this is automatic. DNS and SSL do not require separate paid products.

Choose one canonical hostname, then redirect the other public versions to it:

www.tkalp.dev/*       → https://tkalp.dev/:splat
project.pages.dev/*   → https://tkalp.dev/:splat

Cloudflare’s Bulk Redirects can preserve the path suffix and query string. A request for:

https://www.tkalp.dev/post/hello/?source=rss

then lands on:

https://tkalp.dev/post/hello/?source=rss

That avoids duplicate versions of the same page and keeps shared links predictable.

What publishing looks like

Once Git integration is enabled, publishing becomes almost uneventful:

  1. Write or edit a Markdown post.
  2. Preview it locally with hugo server.
  3. Commit the change.
  4. Push to main.
  5. Cloudflare installs the pinned Hugo version.
  6. It runs the build command.
  7. The contents of public are deployed automatically.

A useful local check is:

hugo --gc --minify

If that command works locally, the Cloudflare build will usually behave the same way. The Pages dashboard keeps build logs and previous deployments, so a broken release is easy to diagnose or roll back.

The actual cost

For a purely static personal blog, the running cost can be close to zero.

ComponentTypical cost
HugoFree and open source
GitHub repositoryFree for this use case
Cloudflare Pages static hostingFree
DNS and SSLFree
Bulk RedirectsIncluded in the Free plan
Custom domainAnnual registration and renewal

Cloudflare’s Free plan currently includes up to 500 builds per month, far more than a personal blog normally needs. Purely static asset requests are free and unlimited; Pages Functions instead count toward Workers quotas.

There is also no surprise compute bill because this setup does not need Pages Functions, Workers, R2, image transformations, or a database. Those products can be useful later, but they are not required for a straightforward blog.

A few SEO basics

Static generation handles much of the plumbing, but it is worth checking the output before calling the site finished:

  • every page has a self-referencing canonical URL
  • robots.txt allows normal crawling
  • sitemap.xml contains the final domain
  • RSS links use the final domain
  • internal navigation uses the canonical hostname
  • the www and pages.dev versions redirect to the canonical domain

After launch, add the domain to Google Search Console and submit:

https://tkalp.dev/sitemap.xml

Search Console is useful for spotting indexing errors, checking search queries, and confirming that Google can crawl the site.

Small gotchas worth avoiding

Building with the wrong Hugo version

Themes sometimes rely on a particular Hugo release. Pin HUGO_VERSION in Cloudflare rather than relying on whichever version happens to be the default.

Forgetting the production base URL

A site can look fine while quietly generating canonical links and feeds for the wrong hostname. Set the domain in config.toml and include it in the Cloudflare build command.

Publishing generated files to Git

The public and resources directories are build output. In most setups they should be ignored by Git and rebuilt on every deployment.

Adding dynamic features too early

Comments, forms, server-side search, and analytics can introduce third-party services or dynamic compute. Add them only when they solve a real problem. A client-side search index is often enough for a small blog.

Forgetting the non-canonical domains

The apex domain, www, and default pages.dev URL may all serve the same content unless redirects are configured. Pick one public address and redirect everything else to it.

Final thoughts

This stack is a good fit for anyone who wants to own their content without turning a blog into a small infrastructure project.

Hugo keeps the content portable. GitHub provides version history. Cloudflare Pages handles builds, TLS, caching, and global delivery. The workflow stays simple: Markdown in, static files out.

Most importantly, it stays cheap. There is no server waiting for traffic and no monthly hosting plan to justify. For a straightforward personal blog, the domain renewal can genuinely be the only recurring bill.

Sources