How to Deploy a Hugo Site to Cloudflare Pages With Github Actions

We recently moved this website from Django to a static website made using Hugo, and decided to use Cloudflare Pages to deploy it. Cloudflare Pages offers automatic deployments, meaning it can automatically build and deploy a website when changes are pushed to a GitHub branch. This is simple to set up and works well. However, we wanted to build and deploy using GitHub Actions instead, so that the build logs would be easily accessible on GitHub. Cloudflare’s wrangler-action makes it simple to set up a GitHub Actions workflow for this.

  1. Create a Cloudflare Pages project, with automatic deployments disabled.

  2. Create an API token and add it, along with your account ID, as secrets in your GitHub settings. See Cloudflare’s docs for details.

  3. Create a YAML file in the .github/workflows directory in your Git repository with the content below. There are some comments with more details. Remember to replace “your-project-name” with the name of your Cloudflare project.

    on:
      # This will deploy the production site when changes are pushed to the "main" branch.
      # The branch name should be the one you set up as the "Production branch" in your Cloudflare Pages project.
      push:
        branches: [main]
      # This will deploy preview sites when a PR is opened/reopened and when changes are pushed to the PR.
      pull_request:
      # Allow manual deploys via the GitHub Actions Web UI.
      workflow_dispatch:
    
    jobs:
      deploy:
        runs-on: ubuntu-latest
        name: Deploy
        steps:
          - uses: actions/checkout@v4
            with:
              # Fetch all history for Hugo's .GitInfo and .Lastmod
              fetch-depth: 0
    
          - name: Cache Hugo resources
            uses: actions/cache@v3
            with:
              path: resources/_gen
              key: ${{ runner.os }}-hugo-resources-${{ github.ref_name }}-${{ github.sha }}
              restore-keys: |
                ${{ runner.os }}-hugo-resources-${{ github.ref_name }}-
                ${{ runner.os }}-hugo-resources-main-
    
          - name: Cache node_modules
            uses: actions/cache@v3
            with:
              path: node_modules
              key: ${{ runner.os }}-node-${{ github.ref_name }}-${{ hashFiles('**/package-lock.json') }}
              restore-keys: |
                ${{ runner.os }}-node-${{ github.ref_name }}-
                ${{ runner.os }}-node-main-
    
          - name: Setup Hugo
            uses: peaceiris/actions-hugo@v3
            with:
              # Update the Hugo version if necessary.
              hugo-version: "0.148.2"
              extended: true
    
          - name: Build
            run: hugo --minify --logLevel=debug
    
          - name: Deploy
            id: deploy
            uses: cloudflare/wrangler-action@v3
            # Update 'your-project-name' below
            with:
              apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
              accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
              # Deploy using `page deploy` wrangler command
              # See: https://developers.cloudflare.com/workers/wrangler/commands/#deploy-1
              # `github.head_ref` will only be available during a `pull_request` event, and will
              # contain the name of the current branch. `github.ref_name` will be available for all
              # events and will also contain the current branch's name, but during a `pull_request`
              # its format is `<pr_number>/merge` which is not what we want.
              # See: https://docs.github.com/en/actions/reference/workflows-and-actions/contexts#github-context
              command: pages deploy public --project-name=your-project-name --commit-dirty --branch ${{ github.head_ref || github.ref_name }}
    
          - name: Print preview URL
            run: echo "${{ steps.deploy.outputs.deployment-url }}"
    
          - name: Print branch preview URL
            run: echo "${{ steps.deploy.outputs.pages-deployment-alias-url }}"
    
          - name: Get short SHA of latest commit in PR
            id: short-sha
            if: github.event_name == 'pull_request'
            run: |
              HEAD_SHA="${{ github.event.pull_request.head.sha }}"
              echo "short_sha=${HEAD_SHA:0:7}" >> $GITHUB_OUTPUT
    
          - name: Comment on PR
            uses: thollander/actions-comment-pull-request@v3
            if: github.event_name == 'pull_request'
            # Update 'your-project-name' below
            with:
              message: |
                ## Deployed your-project-name with Cloudflare Pages
    
                | **Latest commit:** | `${{ steps.short-sha.outputs.short_sha }}` |
                |:-|:-|
                | **Status:** | ✅ Deploy successful! |
                | **Preview URL:** | ${{ steps.deploy.outputs.deployment-url }} |
                | **Branch Preview URL:** | ${{ steps.deploy.outputs.pages-deployment-alias-url }} |
              comment-tag: cf-preview-url
    

That’s it! Now any time a PR is opened, a preview site will be deployed, and a message will be posted in the PR showing the URLs you can use to access the preview site: A Sample PR Message

When changes are pushed to the PR, the preview site will be redeployed, and the same message will be updated.

And when you merge the PR into your production branch, the production site will be deployed.