Hugo

Deploying Hugo to Cloudflare Pages with GitHub Actions

Cloudflare Pages is one of the best hosting targets for Hugo sites. It is fast, globally distributed, free for most publishing workloads, and integrates cleanly with GitHub repositories. You can deploy directly through Cloudflare’s built-in Git integration — or through a GitHub Actions workflow for more control over the build process. This guide covers both approaches and when to choose each.

Option 1: Cloudflare’s Direct Git Integration

The simplest path requires no GitHub Actions configuration. Connect your repository directly to Cloudflare Pages:

  1. Log in to the Cloudflare dashboard → Pages → Create a project
  2. Connect to Git → Select your GitHub repository
  3. Configure the build:
    • Framework preset: Hugo
    • Build command: hugo
    • Build output directory: public
  4. Set environment variables:
    • HUGO_VERSION = 0.145.0 (or your target version)
    • HUGO_ENV = production

Cloudflare automatically builds and deploys on every push to your configured branch. Pull requests get preview deployments at unique URLs — useful for reviewing content or theme changes before merging.

When to use this: For most Hugo publishing sites, the direct integration is all you need. It is zero-configuration from the repository side and handles previews automatically.

Option 2: GitHub Actions Deployment

For cases where you need more control — running additional build steps (Pagefind indexing, image processing scripts, content validation), using a specific Hugo version not available in Cloudflare’s preset, or coordinating deployments across multiple repositories — a GitHub Actions workflow gives you that control.

Create .github/workflows/deploy.yml:

name: Deploy to Cloudflare Pages

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      deployments: write

    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          submodules: recursive  # Required if theme is a Git submodule
          fetch-depth: 0         # Full history for .Lastmod to work correctly

      - name: Setup Hugo
        uses: peaceiris/actions-hugo@v3
        with:
          hugo-version: '0.145.0'
          extended: true         # Required for Sass/SCSS support

      - name: Build
        run: hugo --minify --environment production
        env:
          HUGO_ENV: production

      - name: Index with Pagefind
        run: npx pagefind --site public

      - name: Deploy to Cloudflare Pages
        uses: cloudflare/pages-action@v1
        with:
          apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
          projectName: your-project-name
          directory: public
          gitHubToken: ${{ secrets.GITHUB_TOKEN }}
          branch: ${{ github.ref_name }}

Setting Up Secrets

The workflow needs two Cloudflare credentials stored as GitHub repository secrets:

CLOUDFLARE_API_TOKEN: Create at dash.cloudflare.com → Profile → API Tokens → Create Token. Use the “Edit Cloudflare Workers” template and scope it to your account. More precisely, it needs Cloudflare Pages: Edit permission.

CLOUDFLARE_ACCOUNT_ID: Found in your Cloudflare dashboard URL — dash.cloudflare.com/{account-id}/ — or in the right sidebar of any Zone page.

Add both at GitHub → Repository → Settings → Secrets and variables → Actions.

Handling Hugo Themes as Submodules

If your theme is installed as a Git submodule, the submodules: recursive option in the checkout step handles initialization. If you have not initialized the submodule locally:

git submodule add https://github.com/theme-author/theme-name themes/theme-name
git commit -m "Add theme submodule"

Alternatively, vendor the theme directly by copying it into themes/ and committing the files without a submodule relationship. This avoids submodule management at the cost of losing automatic upstream updates.

Branch Previews

The pages-action automatically creates preview deployments for pull requests using the branch name. Cloudflare Pages deploys each branch to a unique preview URL (branch-name.your-project.pages.dev), and the GitHub Actions integration posts the preview URL as a commit status on the pull request.

This enables a complete editorial preview workflow: create a branch, write a post, open a pull request, and share the preview URL for review — all before merging to the main branch and triggering the production deployment.

Caching for Faster Builds

For sites with large image caches or heavy Hugo Pipes processing, caching the resources/ directory between workflow runs speeds up builds significantly:

      - name: Cache Hugo resources
        uses: actions/cache@v4
        with:
          path: resources
          key: ${{ runner.os }}-hugo-resources-${{ hashFiles('assets/**') }}
          restore-keys: |
            ${{ runner.os }}-hugo-resources-            

      - name: Build
        run: hugo --minify --environment production

The cache key includes a hash of the assets/ directory. When assets change, the cache is invalidated and rebuilt. When assets are unchanged, Hugo restores the processed resources and only rebuilds changed content.

Environment Variables in Hugo Templates

Environment variables set in the Cloudflare Pages dashboard or the GitHub Actions workflow are accessible in Hugo templates through os.Getenv:

{{ $analyticsID := os.Getenv "ANALYTICS_ID" }}
{{ with $analyticsID }}
  <!-- Analytics snippet using the ID -->
{{ end }}

Set non-secret configuration variables in the Cloudflare Pages dashboard under Settings → Environment variables. Use GitHub Actions secrets for values that should not appear in workflow logs.

Monitoring Deployments

Cloudflare Pages provides a deployment log for every build under the Pages project dashboard. Failed deployments surface the build output, making it straightforward to diagnose issues.

For the GitHub Actions path, the workflow run log is available in the repository’s Actions tab. Failed steps surface the relevant error output.

Custom Domains

Point a custom domain at your Cloudflare Pages project under Project → Custom domains → Set up a custom domain. If the domain is already on Cloudflare, the DNS configuration happens automatically. For external domains, add the provided CNAME record at your registrar.

Cloudflare Pages provisions and auto-renews SSL certificates for all custom domains.