31.3 Cloudflare Pages: Hugo Integration and Edge Caching
Alright, let’s talk about Cloudflare Pages. You’ve probably heard the buzz: it’s free, it’s fast, and it’s got that sweet, sweet global edge network. And for the most part, the hype is real. Deploying a Hugo site here feels like putting a rocket engine on a go-kart—in the best way possible. But, as with all things that seem too good to be true, there are a few quirks you need to understand so you don’t bash your head against a wall later. I’ve been there, so you don’t have to.
The Basic wrangler.toml and Build Configuration
Forget complicated YAML files. Cloudflare Pages uses a wrangler.toml file for configuration, but honestly, for a basic Hugo site, you can often get away with just setting things in the GUI. The real magic is in the build settings you configure, either in the dashboard or via a wrangler.toml file placed in your repository root.
Here’s a minimal, functional wrangler.toml to start with:
[[pages_builds]]
name = "your-awesome-hugo-site"
production_branch = "main"
compatibility_date = "2024-05-01"
[pages_builds.build]
command = "hugo --minify"
output_dir = "public"
Let’s break this down. The command is what Cloudflare’s build environment will execute. We’re using hugo --minify because we’re not savages; we want our HTML tidy. The output_dir is crucial—it’s always public for Hugo. This is non-negotiable. It’s where Hugo vomits out all your beautifully rendered static files, and it’s where Cloudflare Pages expects to find them. Get this wrong, and you’ll deploy a thrillingly empty website.
The compatibility_date is a Cloudflare quirk. It locks your build to a specific set of APIs and behaviors from that date. It’s their way of ensuring backward compatibility. Just set it to something recent and you’re golden.
The Quirks of Hugo Version Pinning
Here’s the first “gotcha.” Cloudflare’s build environment has a default version of Hugo installed. You have absolutely no idea what it is. It might be the latest, it might be a version that was new when Napoleon was crowned. Relying on it is a recipe for “but it works on my machine!” despair.
The solution is brilliantly simple: pin your version using a .hugo_version file in your repo. This file contains exactly one thing: the Hugo version number you want.
echo "0.128.0" > .hugo_version
Commit this file. Now, Cloudflare’s build process will automatically fetch and use Hugo 0.128.0. You’ve eliminated a massive variable. You’re welcome. Always, always, always do this. It’s the single most important best practice for Hugo on Cloudflare Pages.
Configuring Your baseURL for Production
This one trips up everyone. Your local development uses http://localhost, but your live site is at https://your-cool-site.pages.dev. If you don’t tell Hugo the correct production URL, your builds will have all the wrong absolute paths. The solution is to use Hugo’s environment-based configuration.
In your hugo.toml (or config.toml), set your production baseURL. Then, override it for development. Here’s the clean way to do it:
hugo.toml:
baseURL = 'https://your-production-domain.com'
languageCode = 'en-us'
title = 'My Awesome Site'
[imaging]
resampleFilter = 'catmullrom'
quality = 80
config/production/config.toml:
baseURL = 'https://your-production-domain.com'
[build]
writeStats = true
config/development/config.toml:
baseURL = 'http://localhost:1313/'
When you run hugo server, it uses the development config. When Cloudflare Pages runs hugo (which defaults to the production environment), it uses the correct production baseURL. This keeps your local and deployed builds sane.
Leveraging Edge Functions and Redirects
This is where Cloudflare Pages starts to flex. You can add a _redirects file in your static directory (e.g., static/_redirects). Hugo will copy it directly to your output, and Cloudflare will parse it for redirects, rewrites, and proxies at the edge. This is insanely powerful.
# static/_redirects
/blog/my-old-post-1 /blog/my-new-post-slug 301
/blog/my-old-post-2 /blog/another-new-post 302
/api/* https://legacy-api.example.com/api/:splat 200
/news/* /blog/:splat 301
The real magic is the 200 status (proxy) on the /api/* line. You can use this to mask an external API, making it look like it’s part of your site, all without a single line of server code. The request is handled at the data center closest to your user. It’s brutally efficient.
You can also get more advanced with Cloudflare Pages Functions for proper serverless functions, but for most Hugo sites, the _redirects file is more than enough power.