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.
Building the Workflow 🔗
This is all built around creating a GitHub Actions workflow. It will do the following:
-
Trigger when a new PR is created
-
Checkout the code from the PR source branch
-
Install the dependencies on the container that’s running
-
Set a variable to hold the target URL for the deployed preview
-
Build the Hugo site
-
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:
These break down as follows:
-
repository.owner
:rmoff
-
repository.name
:rmoff-blog
-
job.name
: This is the name of the job, specified underjobs:
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 🔗
-
Create a branch of the main branch, into which you will commit a new blog post:
git checkout -b my_new_article
-
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
-
Create a PR from the new branch:
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
-
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
-
Bask in the glory of a nice preview of your new article, whilst safely not impinging upon your live site.
My thanks to Gunnar Morling for basically figuring all this out for me to steal seek inspiration from.