Recently I revamped my open source version of 2048 Game and decided to migrate it to Next.js and React 18. The existing game was published to GitHub Pages without any custom domain. I was considering to deploy to Vercel but it would lose organic traffic from Google which was build up over the last 3 years. It means I needed to experiment with deployment to GitHub Pages and today I will share what i learnt.
If you want to see the end result of before reading the whole article, you can
I will be using two GitHub features – GitHub Actions and GitHub Pages. If you haven't heard of them, let me quickly explain:
GitHub Actions are like little workflows that can do tasks on your projects. It's like having a helper that automatically does things you tell it to do. You can use Actions to run tests, for quality checks, or to build your application. In my case, I used this workflows to automate deployment pipeline.
What are GitHub Pages? Think of them like a web hosting option for developers and open source projects. You can use GitHub Pages to share your portfolios, host websites of your open-source projects, or just publish your pet projects like mine.
Now let's get started.
To publish our Next.js application, I needed to activate GitHub Pages for the project’s repository. You can find in the Settings tab (1 in the image below), then select Pages from the menu on the left-hand side (2), and find the dropdown menu that allows us to specify the deployment Source (3).
Now you will need to change the deployment Source to GitHub Actions.
From now on, your project has a dedicated page. You only need to publish content there.
Before deploying the Next.js app, it's important to change the build output. By default, Next.js uses Node.js to run the application, and this is incompatible with GitHub Pages.
GitHub Pages is designed to host static files, which means we can publish only HTML, CSS, JavaScript (and other static files) there. So we'll need to enable static page generation in Next.js.
To do so, you will change the output mode to export
inside next.config.js
:
/** @type {import('next').NextConfig} */
const nextConfig = {
output: "export", // <=== enables static exports
reactStrictMode: true,
};
module.exports = nextConfig;
Now after running next build
, Next.js will generate an out
folder containing static assets for your app. In the next steps, we will take this directory and upload it to GitHub Pages.
The Pages are published under a sub-path of a domain and takes the project name as a sub-path. Confusing? Let's take a sneak peak into a URL of my 2048 game as an example:
https://mateuszsokola.github.io/2048-in-react/
Github created a dedicated subdomain for my user called mateuszsokola (my username). But the project is published under the sub-path, which in my case is /2048-in-react
. Unfortunately, this will lead to issues with missing images and styles.
By default, Next.js maps all static assets the domain. This means that the favicon.ico
file will be resolved to mateuszsokola.github.io/favicon.ico
instead of mateuszsokola.github.io/2048-in-react/favicon.icon
.
To fix this, we can set up a path prefix by adding basePath
inside the next.config.js
file:
/** @type {import('next').NextConfig} */
const nextConfig = {
basePath: "/2048-in-react", // <=== here it is
output: "export",
reactStrictMode: true,
};
module.exports = nextConfig;
In my case, it is /2048-in-react
since my project is called 2048-in-react
.
Remember to include the (/
) at beginning of the project directory.
Next.js is producing deployment artifacts that can be published to GitHub Pages. Now it's due time to set up Github Actions to publish them. I decided the deployment into two separate actions to promote reusability:
The setup-node
action will set up Node.js and instal all dependencies. Having a standalone action for the Node.js setup will allow me to reuse it for other pipelines. For example, I have pipelines that run
The publish
action will build Next.js artifacts and publish them to GitHub Pages each time we merge code into the main
branch.
Let me begin by explaining the setup-node
action. Here is the code:
# File: .github/workflows/setup-node/action.yml
name: setup-node
description: "Setup Node.js ⚙️ - Cache dependencies ⚡ - Install dependencies 🔧"
runs:
using: "composite"
steps:
- name: Setup Node.js ⚙️
uses: actions/setup-node@v4
with:
node-version: 20
- name: Cache dependencies ⚡
id: cache_dependencies
uses: actions/cache@v3
with:
path: node_modules
key: node-modules-${{ hashFiles('package-lock.json') }}
- name: Install dependencies 🔧
shell: bash
if: steps.cache_dependencies.outputs.cache-hit != 'true'
run: npm ci
Important: Create this file in the .github/workflows/setup-node
directory in your project. Make sure to call it action.yml
.
What does this snippet do?
It creates a composite
action. The composite
action allows you to bundle multiple workflow steps into a single action. If it isn’t clear you will understand it once we get into the second action.
It creates a new build environment using Node.js 20 and installs project dependencies.
These are the most important parts of the setup-node
action. Now, let's move on to the publish
action:
# File: .github/workflows/publish.yml
name: publish-to-github-pages
on:
push:
branches:
- main
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: "pages"
cancel-in-progress: false
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout 🛎️
uses: actions/checkout@v4
- name: Setup Node.js ⚙️ - Cache dependencies ⚡ - Install dependencies 🔧
uses: ./.github/workflows/setup-node
- name: Setup Pages ⚙️
uses: actions/configure-pages@v4
with:
static_site_generator: next
- name: Build with Next.js 🏗️
run: npx next build
- name: Upload artifact 📡
uses: actions/upload-pages-artifact@v3
with:
path: ./out
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
needs: build
steps:
- name: Publish to GitHub Pages 🚀
id: deployment
uses: actions/deploy-pages@v4
Create this file in the .github/workflows
directory in your project. You can name the file as you like – I called mine publish.yml
.
What it does?
This action is triggered each time the new code is pushed or merged into the main
branch.
It uses the setup-node
action to set up the environment. The composite
action I created in the previous action. Now you know how to include your composite
actions in other actions.
The action has two stages: in the first stage, Next.js app is being built. In the second stage, the artifacts from the first stage are uploaded to GitHub Pages.
These are the most important aspects of the deployment pipeline. I skipped the permissions and concurrency setup since they remain unchanged for all GitHub Pages deployments.
Now, action is ready to use.
After committing and pushing your changes to the main
branch, GitHub will automatically spin up the deployment to GitHub Pages.
To inspect the process, navigate to the Actions tab (1 in the image below), and select the publish-to-github-pages action from the menu on the left hand side (2). You will see a all your deployments on the screen (they are called workflows).
Now select the first one of those workflows, and you will see a two-stage deployment. In the deploy stage, you can find a link to your website on GitHub Pages.
Github Pages isn't sufficient for hosting websites with millions of views. But it's an perfect choice to host your portfolio or a website for your open-source project.
There are many free options to host our websites such as Vercel, but I wanted to show you an alternative. GitHub Pages is built for developers and I think every developer should be familiar with it.
If this article helped you, please share it on your social media.
And where do you deploy your application? Is it always Vercel?
You don’t feel strong with React or Next.js? Join my online course on Udemy! I will help you to get started with React by creating a fully-functional 2048 Game. I believe creating games makes learning more fun, and you'll have something cool to show your friends.
👇👇👇👇
🧑🎓 Join