Publish a static site to Cloudflare
When publishing static websites like ones generated with the help of Hugo or Zola to Cloudflare, the recommendation in the docs is to use Cloudflare Workers for new projects instead of Cloudflare Pages as mentioned in e.g. zola docs.
There are however a couple of gotchas to watch out for when using Workers.
Features
First, Cloudflare Workers provide a lot more features geared towards dynamic workloads, see the comparison in the migration guide. That also means there are a lot of things in the Workers docs that won't apply or you won't need for static site projects.
The Migration Guide is aimed at, well, migrations, but it has good examples of things you may need in your configuration for static pages.
C3 aka Create Cloudflare CLI
The C3 command, aka the create-cloudflare package supports static
websites with --type=hello-world-assets-only
option and creates a
packages.json
where you can specify version of wrangler to use (this is
generally optional if you don't want to use NPM or other Node.js package
managers), and a wrangler.jsonc
config file.
Note that C3 doesn't support Hugo nor Zola in its --framework
option so
you will need to configure the details manually.
Wrangler
Wrangler is Cloudflare's CLI for the general developer platform which includes
both Workers and Pages. It supports static websites OK, but many of its
features are designed for supporting dynamic workers so while you may often see
references to config options like main
which specifies the worker's
entrypoint, or operations like bundling which will create an easy to upload
bundle from all your worker's files, just note that these are aimed for dynamic
workers.
For static websites we generally rely on the fact that by default requests
matching static asset files in the directory specified by assets.directory
configuration will be served directly without invoking worker code,
which means that these features do not apply.
Wrangler Config
Wrangler will be used to deploy your code, whether you're deploying manually,
using an external CI/CD system, or using Cloudflare's CI/CD (default
deploy commands use wrangler deploy
/wrangler versions upload
as well, it's
just they will be invoked from a CI/CD pipeline run). So you will generally want
a Wrangler config file in your repo - wrangler.toml
or wrangler.jsonc
. These
options should suffice:
name = "worker-name-should-match-name-in-dashboard"
compatibility_date = "2025-06-26" # for new projects set to creation date
# routes or custom domains config (if not using `workers.dev` domain)
routes = []
[assets]
# both Hugo and Zola produce the built site in public/ subdir
directory = "./public/"
# this is not auto-detected based on existance of 404.html as in Pages
not_found_handling = "404-page"
[build]
# see below
command = "..."
Build Command
At the time of writing the
image
used for CI/CD builds did not have support for Zola built-in, but it does have
Hugo. So for Hugo you should be able to just use hugo
as build command in
config. For Zola we need to install it first but we can use asdf for that:
[build]
command = "asdf plugin add zola https://github.com/salasrod/asdf-zola && asdf install zola 0.20.0 && asdf global zola 0.20.0 && zola build"
Or put that in a script, chmod +x it, and use that as the command.
There is an issue open about adding support for Zola to the build image.
Also note that this command is referred to as custom build in Cloudflare
docs and is automatically executed when wrangler deploy
is run. It is best for
it to be idempotent. See also below.
Preview Envs
Cloudflare CI/CD has builds for non-production branches
enabled by default, and will use the non-production deploy command
in these cases (npx wrangler versions upload
by default). This will then
deploy the site at a build-specific preview URL like
{ID}-{WORKER_NAME}.{ACCOUNT}.workers.dev
(preview URLs on subdomains other
then workers.dev
are not currently supported, see limitations).
In a Zola static site it is currently recommended to use the full
domain the site will be deployed at in base_url
setting in the configuration.
In preview envs however, this will cause some links to be generated in a way
that they point back to the main site instead of the preview instance. Still,
since the preview envs are exposed at the root of the subdomain, we can use a
script in the build command that detects if we're building for a preview env and
overrides the base URL to /
.
Create a cfbuild.sh
and chmod +x
it:
set -euo pipefail
ZOLA_VER="0.20.0"
function ensure_deps() {
if type -p zola &>/dev/null; then
return
fi
if ! type -p asdf &>/dev/null; then
echo "asdf package manager missing from the build env" >&2
fi
asdf plugin add zola https://github.com/salasrod/asdf-zola
asdf install zola "${ZOLA_VER:?}"
asdf global zola "${ZOLA_VER:?}"
}
function zola_build() {
local -a bld_args=()
if [[ ${WORKERS_CI_BRANCH:-main} != "main" ]]; then
bld_args+=( --base-url "/" )
fi
zola build "${bld_args[@]}"
}
function main() {
ensure_deps
zola_build
}
main "$@"
Then change the build command in wrangler.toml
to:
[build]
command = "./cfbuild.sh"
Bootstrapping
Cloudflare docs recommend treating the Wrangler config file in your repo as the
source of truth for the Worker configuration, and generally whenever
wrangler deploy
is used either explicitly or implicitly (e.g. in CI/CD), it
will push options set in the config file, overwriting any changes made via the
dashboard.
So, if using manual deploy or external CI/CD, you can just set all the options
through the config and use wrangler deploy
.
However if you want Cloudflare CI/CD to automatically build and deploy your
project you'll need to set it up via the dashboard initially, so that you can
connect Cloudflare to your Git host. And when setting up the Worker
through the dashboard, it won't automatically pick up your Wrangler config file
stored in the repo, so you will need to set enough configuration through the
dashboard for the build to succeed (if you have build command
configured as above, just the project name should suffice). When the build runs,
it will run wrangler deploy
which will then push all the other options from
the config file to Cloudflare, and from then on you can manage all the option
changes through the file.
Also note that the build command set in Worker configuration in the dashboard is separate from the build command set in Wrangler config and it runs before Wrangler is executed. So during a build both commands will actually be executed. This is why it's a good idea for the build command to be idempotent, or you can just unset the build command in the dashboard so only the Wrangler command is used.
Pages
Note that Cloudflare Pages aren't going away, it's just the development going forward will be focused on Workers so Pages won't be receiving many new features.