Deploy NEXT.JS App to GitHub Pages
These last couple of days I've been wanting to create a simple and straight forward blog, to just keep the articles that I write and present myself. So I decided to do that using GitHub Pages and a simple NEXT.JS application, very similar to the one shown in the official tutorial. It suprised me how little and misleading information I found on how to do that, so I'm writing this. While making this guide I found a video that explains a similar approach to what I did and it's worth taking a look at it! You can check it out here. It would have save me some effort finding this earlier.
Conditions
Before we start, there are cases where this is not going to be possible so I want to save you the trouble of going through all of this just to get a disgusting suprise at the end.
We are going to depend on the output of next export
, so it is important that the application that you want to deploy is able to use it.
With
next export
, we build an HTML version of your app. At export time, we callgetStaticProps
for each page that exports it, and pass the result to the page's component. It's also possible to use the oldergetInitialProps
API instead ofgetStaticProps
, but it comes with a few caveats:
getInitialProps
cannot be used alongsidegetStaticProps
orgetStaticPaths
on any given page. If you have dynamic routes, instead of usinggetStaticPaths
you'll need to configure theexportPathMap
parameter in yournext.config.js
file to let the exporter know which HTML files it should output.- When
getInitialProps
is called during export, thereq
andres
fields of itscontext
parameter will be empty objects, since during export there is no server running.getInitialProps
will be called on every client-side navigation, if you'd like to only fetch data at build-time, switch togetStaticProps
.getInitialProps
should fetch from an API and cannot use Node.js-specific libraries or the file system likegetStaticProps
can.It's recommended to use and migrate towards
getStaticProps
overgetInitialProps
whenever possible.The
fallback: true
mode ofgetStaticPaths
is not supported when usingnext export
.API Routes are not supported by this method because they can't be prerendered to HTML.
getServerSideProps
cannot be used within pages because the method requires a server. Consider usinggetStaticProps
instead.Internationalized Routing is not supported as it requires Next.js' server-side routing.
The
next/image
component's default loader is not supported when usingnext export
. However, other loader options will work.
You can read here for more information.
I will assume that you already have a NEXT.JS app. In order to test if your app will work, go to the package.json
and add an export
script to the scripts
section. It should look something like this:
...
"scripts": {
"dev": "next dev",
"build": "next build",
"export": "next export",
"start": "next start"
},
...
And now if you run npm run build && npm run export
without problems, then your app is ready.
Why?
Why would we try to do something like this if Vercel let's us publish the app without the need of using next export
for free? If it's just a blog, why bother on doing all this to just deploy GitHub Pages? Well, in my case, it was because I wanted to accept Brave Rewards and it turns out that that's against Vercel's fair use policy (I asked the Vercel team).
And I also like the idea of having my blog under .github.io.
GitHub Repository
We can use an existent repository for a project site or we can create one for our user or organization site. You can read here for more information. The first one is a bit more complicated, but we will address the issues that might present later.
I will also assume that the source code for the NEXT.JS app is on the root (/) directory of the main
branch. If that is not the case for you, you will have to make some changes to the things we do to follow along.
Let's create an orphan branch called gh-pages
on our local repository and make sure that the staging area is empty. Then create an empty commit and push the branch to GitHub.
git checkout --orphan gh-pages
git reset
git commit --allow-empty -m "Initial commit"
git push --set-upstream origin gh-pages
Now we go to the Settings tab of our repository on GitHub, to the Pages section and select the branch gh-pages
and the / (root) directory, then we click Save.
We could just push the result of the export
to gh-pages
manually every time we want to update the page. That will work, but it's too much work for every change we want to make and it requires a lot of manual intervention, so we also want this to be automated as much as possible.
GitHub Actions
Let's create a GitHub Action so every time we push our changes to main
, we automatically deploy our app. You can read here if you want to use a different trigger.
We will use the Node.js template, you can find the updated one here. Create a file called gh-pages.deploy.yml (you can actually name it whatever you want) under the .github/workflows directory.
In that file we will modify the Node.js template to suit our needs into something like this.
name: Deploy to GitHub Pages
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [14.x]
steps:
- name: Get files
uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}
- name: Install packages
run: npm ci
- name: Build project
run: npm run build
- name: Export static files
run: npm run export
- name: Add .nojekyll file
run: touch ./out/.nojekyll
- name: Deploy
uses: JamesIves/github-pages-deploy-action@4.1.1
with:
branch: gh-pages
folder: out
This file is a set of instructions for GitHub to run our tasks. Let's review it by parts.
The first name
is just to indicate the name of the workflow, you can change it to whatever you want.
The on
will specify when this workflow should be run. Here we are saying whenever we push to the main branch. You can modify this as needed by changing the branch name or even the trigger action, maybe you want this to be updated on PRs as well like it is on the template.
Then we have jobs
. This section will define the jobs that we want to run on the workflow, here we have just the build
. Inside it we also specify the OS in which it will be ran, the Node versions and the steps. Unless you really need to change the OS, leave it on ubuntu-latest
, you can read more about that here. I selected only the 14.x version of Node because that is what works better for me, but if your project needs a different version, then you can change it.
The steps that we will use are quiet simple. On the first step, the checkout step, we get our code. The second step will setup NodeJS. The third one will install the packages with npm. The fourth one will build our project. The fifth will export the static files for NEXT.JS. The sixth step will create a .nojekyll file inside the directory with the exported files. The last step is extracted from here. It will create a branch, gh-pages
in our case, with the content of the out directory. You can modify that if you need to. The name
tag is optional, but I think it is nicer to look at than just the plain command.
IMPORTANT: keep the indentation of the YAML file, it relies on it (kind of like Python).
You might be wondering what is the empty .nojekyll file that we created. Well, GitHub Pages works with the Jekyll engine (more information here), so there are some directories that will be skipped automatically. Anything that starts with underscore or dot, won't be accessible. Since we need access to _next directory, then we need to bypass Jekyll and the way to do that is by adding that .nojekyll file.
Now we can push our content to the repository and if we check the Actions tab on GitHub, we will see that our workflow is running. Once it's finished, if we go to the GitHub Pages URL (it will be https://YOUR_USERNAME.github.io or https://YOUR_USERNAME.github.io/YOUR_REPOSITORY) we will see our page. If it's a user site, then that's it! We should be able to navigate everything correctly. If it's a project site you might be seeing some issues with the images and static resources. We will fix that next.
Fixing the issues
For project sites these are the problems we will see:
- Images are not loading
- Navigation is not working
To put it short, the reason why this is happening is because NEXT.JS expects to be under https://YOUR_USERNAME.github.io/ while we are under https://YOUR_USERNAME.github.io/YOUR_REPOSITORY. You can read more about this here, here and here.
To fix this we will make use of environment variables. On the example we can see on the NEXT.JS documentation, on the CDN Supoport with Asset Prefix section, it's using the NODE_ENV variable to check if we are on production. We could use something like that but in our case I prefer using the NEXT_PUBLIC_BASE_PATH variable.
We have to first create a file on the root of our app called next.config.js
and put this content inside:
module.exports {
basePath: process.env.NEXT_PUBLIC_BASE_PATH,
assetPrefix: process.env.NEXT_PUBLIC_BASE_PATH
}
Then on the YAML file (the one we named gh-pages.deploy.yml
earlier), we have to add that variable and initialize it with our repository name.
...
jobs:
build:
runs-on: ubuntu-latest
env:
NEXT_PUBLIC_BASE_PATH: /YOUR_REPOSITORY
strategy:
matrix:
node-version: [14.x]
...
Make sure to replace YOUR_REPOSITORY by your actual repository name.
Now we are only left to fix the images, since they are being loaded from the public directory, our prefixes don't have any effect there. In order to fix that we will go to the files where we can create a prefix.js
file and add the following:
const prefix = process.env.NEXT_PUBLIC_BASE_PATH || '';
export { prefix };
Then we just need to import prefix
where we are using references to the images and prepend that to the src
attribute.
...
import { prefix } from '../../utils/prefix.js';
...
<img src={`${prefix}/someimage.jpg`} alt='some alt text' />
...
Commit and push, and once the action is finished you will be able to see the images and navigate your app!
Thanks!
Thanks for reading this article! I hope this was helpful and as fun to read as it was to write!