Publish a static site to Cloudflare

Michael Gill 1159 words 6 minutes Cloudflare Cloudflare Workers Hugo Zola SSG

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.