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.
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:
- Open Custom domains.
- Add the apex domain, such as
tkalp.dev. - Add
www.tkalp.devif it should also work. - 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:
- Write or edit a Markdown post.
- Preview it locally with
hugo server. - Commit the change.
- Push to
main. - Cloudflare installs the pinned Hugo version.
- It runs the build command.
- The contents of
publicare 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.
| Component | Typical cost |
|---|---|
| Hugo | Free and open source |
| GitHub repository | Free for this use case |
| Cloudflare Pages static hosting | Free |
| DNS and SSL | Free |
| Bulk Redirects | Included in the Free plan |
| Custom domain | Annual 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.txtallows normal crawlingsitemap.xmlcontains the final domain- RSS links use the final domain
- internal navigation uses the canonical hostname
- the
wwwandpages.devversions 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.
