Image Courtesy: Growtika
It hadn’t even been a day since I published my second blog on setting up React Router with GitHub Pages, and I was excitedly sharing the link with my boyfriend on Discord.
And well here's the embed it automatically generated...
Yep, that's the crying emoji I felt at that moment. lol.
The preview generated for my blog page was still based on the content of my home page.
I shouldn't have been surprised but I just hadn't concsiously thought about it.
So, in an html
document, there are some meta tags called Open Graph meta tags 1 which control how previews are generated for a url when shared on social media platforms like Discord. Since, my website was mostly operating on Client Side— from triggering a render to rendering the components to Committing it to the DOM 2 , all handled by the Client Side JS as opposed to Server Side JS— I didn't really have the luxury to respond with pre-generated html files from the server with those meta tags already present in them.
Some possible workarounds include creating HTML files with the necessary meta tags for each blog post (each route) manually or automating it with JavaScript. However, implementing this outside of the React SPA and then working our way into the SPA might get tricky, so I didn’t explore that much.
Fortunately, there are great tools to simplify this process, like Next.js . Generating static html files in advance, with included Open Graph meta tags, is a stratergy called Static Site Generation (SSG).
The only catch? Besides the challenge of migrating from React Router to Next Router, my blog metadata is sourced from Contentful CMS. So, if I want to publish a quick metadata change from Contentful, I’d need to rebuild and redeploy everything on GitHub to update the static files. This wasn't particularly an issue with pure client-side rendering, as I was simply using a script to request data from the API during runtime.
Of course I could have done all of that manually too, which wouldn't be very error-prone, owing to the scale of this project. But I was like- it is still inconvenient. What better time than now to streamline this workflow with GitHub Actions!
Gone are the days of hash routing! yay!
No more hacks or side effects to circumvent problems related to it.
Since, whenever we visit a valid route, there is a html file PRESENT for it, which means we no longer need the workarounds I discussed in my previous blog. 3
I’ll keep my earlier post up for anyone still wrestling with those issues, especially if they aren’t too concerned with SEO or social media previews. It can still be a helpful resource for a hash-routing setup.
Alright, let’s dive in.
While I’ve worked with CI/CD setups in past roles, writing workflows myself is a new territory for me.
It’s worth noting that GitHub Workflows can automate a variety of tasks beyond CI/CD—they’re designed to streamline all kinds of GitHub-related jobs.
Let’s take this step-by-step!
Name
1name: Deploy to GitHub Pages
2
Every GitHub Workflow file starts with a name you assign it.
This is the name that appears in the side pane under the actions tab on GitHub.
GitHub will then display a yaml
file name for when you do not provide a name
.
Events
1on:
2 workflow_dispatch:
3 repository_dispatch:
4
The on
keyword defines the events
that trigger your workflow’s jobs
. Events could include actions like committing, pushing, merging, pull requests, creating issues, closing issues, making a new branches, and many more .
For example, for a push
event, your yaml file must look something like this:
1on:
2 push
3
You can also make it more specific by defining criterias such as which branch to listen to, filters, pattern matching, and other options.
workflow_dispatch
enables you to manually dispatch events, by automatically granting you access to "Run Workflow" button in the Actions tab, under the corresponing Workflow.
repository_dispatch
enables you to integerate with third party applications, by allowing them to trigger events.
The meat of Workflows: Jobs
At the core of the workflow lies, none other than the actual sequence of instructions that run in a workflow i.e., Jobs
.
There could be multiple jobs. However, here we are only going to take a look over a simple case.
Here’s what jobs look like:
1jobs:
2 build:
3 // ...
4
This code snippet tells GitHub that the jobs section has started. Each job requires an identifier; in this case, build serves as our job identifier.
You can add multiple jobs in the same workflow, like so:
1jobs:
2 build:
3 // ...
4
5 test:
6 // ...
7
Whenever you run a workflow all the jobs run concurrently. So naturally, they must be independent of each other.
It's under a job we can specify the platform it shall run on .
1jobs:
2 build:
3 runs-on: ubuntu-latest
4 // ...
5
Steps
Now the actual constituent instructions that make up a job are called steps . They are grouped under steps
label.
1jobs:
2 build:
3 runs-on: ubuntu-latest
4
5 steps:
6 - name: Checkout repository
7 uses: actions/checkout@v4
8
9 - name: Setup Node Environment
10 uses: actions/setup-node@v4
11 with:
12 node-version: 22
13 cache: npm
14
15 - name: Install dependencies
16 run: npm install
17
18 - name: Build project
19 run: npm run predeploy
20 env:
21 NEXT_PUBLIC_ACCESS_TOKEN: ${{ secrets.NEXT_PUBLIC_ACCESS_TOKEN }}
22
23 - name: Deploy to GitHub Pages
24 uses: peaceiris/actions-gh-pages@v4
25 with:
26 deploy_key: ${{ secrets.ACTIONS_DEPLOY_KEY }}
27 publish_dir: ./dist
28
I really hadn't intended to overwhelm you with that, but sometimes I know no better.
These steps run sequentially i.e., one after the other.
Alirght, now let's break down that yaml
file I just sent your way lol.
As stated earlier, this part is responsible for starting the jobs sections and a specific job build
.
1jobs:
2 build:
3 runs-on: ubuntu-latest
4
5 // ...
6
7
The part we are majorly concerned with right now, is the section under the steps
label.
Before we dive any further, we must note that there are 2 broad categories of the type of steps:
There are developers out there who have already wrote series of instructions for us, making our job easier, just like library developers.
1 // ...
2
3 - name: Checkout repository
4 uses: actions/checkout@v4
5
6 // ...
7
If we use a pre-written GitHub Action, we use uses keyword.
Checkout v4 , as the name suggests, checks-out your repository under $GITHUB_WORKSPACE
.
The GitHub documentation defines $GITHUB_WORKSPACE
as- the default working directory on the runner for steps, and the default location of your repository when using the checkout action.
Now we can setup our environment in the $GITHUB_WORKSPACE
. And again I use a package to do that instead of manually downloading a system package manager, downloading node, and worrying about all the edge cases.
1 // ...
2
3 - name: Setup Node Environment
4 uses: actions/setup-node@v4
5 with:
6 node-version: 22
7 cache: npm
8
9 // ...
10
Has there ever been a javascript project without node_modules
?
1 // ...
2
3 - name: Install dependencies
4 run: npm install
5
6 // ...
7
We, then, build the project.
1 - name: Build project
2 run: npm run predeploy
3 env:
4 NEXT_PUBLIC_ACCESS_TOKEN: ${{ secrets.NEXT_PUBLIC_ACCESS_TOKEN }}
5
Here's where you can set all the env variables utilized in your app. You can configure these in the repository settings under Secrets and variables:
And then we finally deploy!
1
2 - name: Deploy to GitHub Pages
3 uses: peaceiris/actions-gh-pages@v4
4 with:
5 deploy_key: ${{ secrets.ACTIONS_DEPLOY_KEY }}
6 publish_dir: ./dist
7
Oh wait.
To create a SSH Deploy Key, you can checkout this guide .
Note: Make sure there are no trailing new lines in the key when you paste it on GitHub.
By default, gh-pages do not server _next folder. So to get that to work you just have to add .nojekyll
to the root of you build folder.
You could add the .nojekyll
file directly in your build folder, after the building is done.
touch ./dist/.nojekyll
However, the GitHub Pages Action offer an even cleaner solution:
1
2 - name: Deploy to GitHub Pages
3 uses: peaceiris/actions-gh-pages@v4
4 with:
5 deploy_key: ${{ secrets.ACTIONS_DEPLOY_KEY }}
6 publish_dir: ./dist
7 enable_jekyll: true
8
Using enable_jekyll: true
skips the need for .nojekyll
in the build folder, and I find it to be a much cleaner approach.
To trigger a dispatch from a third-party app like Contentful, you’ll need to make a POST request to GitHub's repository dispatch endpoint .
Here it says,
OAuth app tokens and personal access tokens (classic) need the repo
scope to use this endpoint.
So we are first gonna generate one Classic personal access token. We go to GitHub > Profile > Settings > Developer settings > Personal access tokens > Tokens (classic)
And there you can generate a new token.
Generate a new token and ensure it includes the repo scope.
Copy this token somewhere safe, as it won’t be visible again after you leave the page.
Time to headover to Contentful!
In the space settings, locate Webhooks and create a new webhook.
Give it a name and the GitHub API endpoint https://api.github.com/repos/{owner}/{repo}/dispatches
.
Scroll down, and choose what actions on contentful do you want to trigger the GitHub workflow.
Then, set the header.
Give it the the Authorization token we generated and make sure you use the right syntax for the value i.e.,
Bearer <GITHUB_PERSONAL_ACCESS_TOKEN>
.
All these headers are well documented in the repository dispatch endpoint docs .
Except User-Agent
.
It took me couple of unsuccessful triggers to realise we also need an User-Agent header so that GitHub can identify the user or the application that is making the request.
I personally went with my on GitHub username.
The body requires a event_type
parameter, where you can name your custom Webhook event
.
Now, update your deploy.yml
file to trigger the workflow only when publish_contentful_event occurs:
1on:
2 workflow_dispatch:
3 repository_dispatch:
4 type: [publish_contentful_event]
5
Here we change the yaml
file to make it so that the workflow run is triggered by publish_contentful_event
.
You can optionally also accept client_payload
for other information, but make sure to also include it in the Contentful Webhook payload.
For debugging purposes, do not forget to checkout the activity log section of Webhook:
Alas!
So finally, I don't have to bother myself with the hassle of opening my VSCode, running build scripts and deploying manually. Or even ever open GitHub to run workflow manually.
Now, I can create, edit, and delete blogs all in one place. With a single Publish click, the entire workflow takes care of itself. ✨
Facebook Archive. Open Graph Protocol ↩
React Documentation. Render and Commit ↩
Karunika, 2024. This is what happens when you use react-router with Github Pages ↩
Copyright © 2024 Karunika