rmoff's random ramblings
about talks

Using GitHub Actions to build automagic Hugo previews of draft articles

Published Apr 6, 2022 by in Hugo, GitHub, Blogging, GitHub Actions at https://rmoff.net/2022/04/06/using-github-actions-to-build-automagic-hugo-previews-of-draft-articles/

This blog is written in Asciidoc, built using Hugo, and hosted on GitHub Pages. I recently wanted to share the draft of a post I was writing with someone and ended up exporting a local preview to a PDF - not a great workflow! This blog post shows you how to create an automagic hosted preview of any draft content on Hugo using GitHub Actions.

This is useful for previewing and sharing one’s own content, but also for making good use of GitHub as a collaborative platform - if someone reviews and amends your PR the post gets updated in the preview too.

I wrote previously about using GitHub Actions to automagically build and deploy my blog whenever I push new content to the GitHub repo that hosts the source. My friend Gunnar Morling does not only do that with his blog, but he also has a neat preview functionality for new content in a Pull Request (PR). I’m writing this blog to document the steps I took to mimic his setup :)

In essence it’s the same as deploying the production Hugo site to GitHub Pages, except it’s hosted on https://surge.sh/ instead and with a variable URL (since you might have multiple drafts each with their own PR).

Setting up an account on surge.sh 🔗

https://surge.sh/ is not a shell script as the name may suggest, but a hosted service that provides Static web publishing for Front-End Developers. Their audience is denoted very clearly by the fact you can’t sign up online - they assume that of course you’ll have npm installed (because you’re a front-end developer, right). For us heathens without it, you need to install npm first which you can do via brew (which in turn you can install using npm…only kidding)

brew install npm

Then install surge

npm install --global surge

With it installed you can then create an account

$ surge login

   Login (or create surge account) by entering email & password.

          email: robin+surge@moffatt.me
       password:

   Success - Logged in as robin+surge@moffatt.me.

Check your email and verify your email address, and then go and create an authentication token:

$ surge token

   4fimxlh7xmvvs72qhmrmgbhx4jwudepa

Add this to your GitHub repository as a Repository Secret under Actions.

gha token

Building the Workflow 🔗

This is all built around creating a GitHub Actions workflow. It will do the following:

  1. Trigger when a new PR is created

  2. Checkout the code from the PR source branch

  3. Install the dependencies on the container that’s running

  4. Set a variable to hold the target URL for the deployed preview

  5. Build the Hugo site

  6. Deploy the built preview to https://surge.sh/

Let’s look at each of these in detail. Each section contributes a lump of YAML which builds the overall workflow.

Triggering a GitHub Action on a PR 🔗

There are many events in a GitHub repository that can trigger an action, including a pull_request. You can refine it further using a filter, such as only a pull request that’s been marked as ready for review. Here I’m just going to target PRs that are [re-]opened, which is the default and so per the docs no filter is needed:

on:
  pull_request:

Checkout the code 🔗

This action checks out the code from the PR branch. It can be configured to fetch other branches if required, but per the doc:

Only a single commit is fetched by default, for the ref/SHA that triggered the workflow

    - uses: actions/checkout@v3

Install dependencies 🔗

This is the same as used for the live site (which kinda makes sense, given that this is to be a preview of it)

    - name: Install Ruby Dev
      run: sudo apt-get install ruby-dev

    - name: Install AsciiDoctor and Rouge
      run: sudo gem install asciidoctor rouge

    - name: Setup Hugo
      uses: peaceiris/actions-hugo@v2
      with:
        hugo-version: '0.85.0'

Determining the preview URL 🔗

To deploy to surge.sh I used the same GitHub Action as Gunnar, which is this one. The URL that it deploys to is determined by the source repository and PR number:

https://{{repository.owner}}-{{repository.name}}-{{job.name}}-pr-{{pr.number}}.surge.sh

These break down as follows:

  • repository.owner: rmoff

  • repository.name: rmoff-blog

  • job.name: This is the name of the job, specified under jobs: in the YAML that defines the Workflow.

  • pr.number: The PR number, which can be obtained from a variable in the workflow.

I’m stealing Gunnar’s nice bit of code here to determine the PR number :)

    - name: Setup base URL env var
      run: |
        export PRNUMBER=$(jq --raw-output .pull_request.number "$GITHUB_EVENT_PATH")
        echo BASEURL="https://rmoff-rmoff-blog-preview-pr-"$PRNUMBER".surge.sh/" >> $GITHUB_ENV

Note that export works locally to a step, but to pass the variable between steps you append the setting of it to $GITHUB_ENV

Build the Hugo site 🔗

Instead of simply calling hugo which builds the site, we add a few command line flags to build draft posts (--buildDrafts), future-dated posts (--buildFuture), and also change the base URL (--baseURL) which by default (per the config.yaml) is the live site (rmoff.net).

Note that if the base URL is incorrectly set the resulting built site won’t render properly - you’ll get missing CSS files, weird relative links, etc.

Deploying the built site to surge.sh 🔗

This action deploys the built site (under /public, specified in the dist parameter) from the previous step to surge.sh:

    - name: Preview
      uses: afc163/surge-preview@v1
      id: preview_step
      with:
        surge_token: ${{ secrets.SURGE_TOKEN }}
        github_token: ${{ secrets.GITHUB_TOKEN }}
        dist: public
        failOnError: 'true'
        teardown: true
        build: |
          echo Deploying to surge.sh

The finished workflow 🔗

name: Build and Deploy PR Preview to surge

on:
  pull_request:

jobs:
  build_preview:
    runs-on: ubuntu-22.04
    steps:
    - uses: actions/checkout@v3

    - name: Install Ruby Dev                     
      run: sudo apt-get install ruby-dev

    - name: Install AsciiDoctor
      run: sudo gem install asciidoctor
      
    - name: Install Rouge
      run: sudo gem install rouge -v 3.30.0

    - name: Setup Hugo                           
      uses: peaceiris/actions-hugo@v2
      with:
        hugo-version: '0.105.0'

    - name: Setup base URL env var
      run: |
        export PRNUMBER=$(jq --raw-output .pull_request.number "$GITHUB_EVENT_PATH") 
        echo BASEURL="https://rmoff-rmoff-blog-build_preview-pr-"$PRNUMBER".surge.sh/" >> $GITHUB_ENV

    - name: Report base URL env var
      run: echo "${{ env.BASEURL }}" 

    - name: Build                                
      run: hugo --baseURL "${{ env.BASEURL }}"

    - name: Deploy to surge                            
      uses: afc163/surge-preview@v1
      id: preview_step
      with:
        surge_token: ${{ secrets.SURGE_TOKEN }}
        github_token: ${{ secrets.GITHUB_TOKEN }}
        dist: public
        failOnError: false
        teardown: true
        build: |
          echo Deploying to surge.sh

Put this YAML in a file in your github repository root folder under .github/workflows/.

The workflow in action 🔗

  1. Create a branch of the main branch, into which you will commit a new blog post:

    git checkout -b my_new_article
  2. Start writing your blog. Once it’s ready to publish as a draft, commit it and push to GitHub:

    git add content/post/never_gonna_give_you_up.adoc
    git commit -m "Draft ready for review"
    git push --set-upstream origin my_new_article
  3. Create a PR from the new branch:

    pr1
    pr2

    When you create the PR you’ll notice that a check has been added to it - this is the action that’s configured by the workflow and triggered by the PR being created

    pr2a
  4. As the action runs you’ll see it update the PR with its status, and then a completion message including the URL to which the preview has been uploaded

    pr4
  5. Bask in the glory of a nice preview of your new article, whilst safely not impinging upon your live site.

    preview

My thanks to Gunnar Morling for basically figuring all this out for me to steal seek inspiration from.


Robin Moffatt

Robin Moffatt works on the DevRel team at Confluent. He likes writing about himself in the third person, eating good breakfasts, and drinking good beer.

Story logo

© 2025