Go back

Here's what I did not tell you about my Portfolio Website

October 27th, 2024 8:35 PM

github actions
nextjs
contentful

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...

discord embed blog meta data

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!

No more Hash Routing

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.

GitHub Workflow

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.

side pane action tab workflows

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:

  • Command-line Instructions: Executing shell commands directly.
  • Pre-built GitHub Actions: Using reusable instructions, similar to libraries in programming.

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:

github actions secrets

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.

Building Next App

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.

Setup Contentful Webhook

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.

github personal access token scope

Copy this token somewhere safe, as it won’t be visible again after you leave the page.

Time to headover to Contentful!

contentful settings

In the space settings, locate Webhooks and create a new webhook.

contentful webhook name and url

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.

contentful webhook triggers

Then, set the header.

contentful webhook headers

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.

contentful webhook payload

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:

contentful webhook activity log

Voilà!

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.

contentful publish button

Now, I can create, edit, and delete blogs all in one place. With a single Publish click, the entire workflow takes care of itself. ✨

Footnotes

Copyright © 2024 Karunika