Deploying a React App to GitHub Pages automatically
It’s still a little tricky to deploy a react application to GitHub Pages in 2022. So I will explain some of the nuances for an app created by the create-react-app tool, but you could use the same principles with any other setup.
I assume you are familiar enough with git to commit and push. If not yet, please consider reading The Pro Git Book or any other git or GitHub-explaining book, e.g. GitHub Desktop Docs. I also assume that you already have a GitHub Account.
Ok, so you have written a React App, and it works perfectly on your computer when you run npm start
and go to http://localhost:3000
. Now you would like to share it with the world.
- Enable Pages Deployment. Go to your Repository Settings, then the “Pages” tab. In the section “Build and deployment” select “Source”:
GitHub Actions
(screenshot). - Copy the .github/workflows/deploy.yml file from my demo repository and put it into your project to the same place:
.github/workflows/deploy.yml
. - Replace the PUBLIC_URL on the 28th line with the name of your repository. Perhaps, your repository is called
game
, so you should write:PUBLIC_URL: /game
- If you don’t have a react-router or other routing solution, change the 26th line from this:
to this:run: npm run build && npm run gh-pages:404
run: npm run build
- If you do have react-router or something, add gh-pages:404 line to your
package.json
scripts section:"gh-pages:404": "cp build/index.html build/404.html"
- Go to the “Actions” page of your repository, select the ”Deploy” action, and if it is green, check the
https://<owner>.github.io/<project>
page, where<owner>
is your GitHub username and the<project>
is the name of your repository. E.g. https://isqua.github.io/demo-routing-in-typescript-react-app/.
For those who want to understand, I will describe the meaning of these actions below.
GitHub Pages is a static hosting
You cannot run any server-side code in GitHub Pages, because it’s static-only free hosting. So you can deploy only some HTML, CSS, JavaScript, and media files that will be loaded in users’ browsers. Only the front-end part, you know.
You should also consider the URL where your project will be deployed. You have mainly two free options:
https://<owner>.github.io
, so you have to create a repository named<owner>.github.io
in your profile or organization, e.g. my is called isqua.github.iohttps://<owner>.github.io/<repository>
, so if you have a repository namedgame
and your username ismichel99
, the URL of the hosted project will behttps://michel99.github.io/game/
And yes, you can deploy a repository for <owner>.github.io
and as many repositories for your projects as you want.
Build Your App with its URL in mind
So, you run npm start
and open http://localhost:3000
, and your app works. By this address, your browser downloads some http://localhost:3000/index.html
file. If you look at this source code (right-click on the web page in the browser, and select “View Page Source”), you can see that there are some resources whose URLs start with a slash. It may be something like:
/favicon.ico
/manifest.json
/static/js/bundle.js
If you deploy this app to <owner>.github.io
repository, it will work without additional effort. But if you deploy the app as is to <owner>.github.io/<project>
, it may be broken:
- You go to
https://<owner>.github.io/<project>
, and theindex.html
file will be loaded fine. - But it has the
<script src="/static/js/bundle.js"></script>
line. - So the file will be requested from
https://<owner>.github.io/static/js/bundle.js
, but there is no such file. The right URL should contains project name:https://<owner>.github.io/<project>/static/js/bundle.js
So when you build your app, you need the file paths to include the project name.
For the bundle.js
this is the wrong URL:
/static/js/bundle.js
And this is the right one:
/<project>/static/js/bundle.js
To achieve this, set the PUBLIC_URL
environment variable when you build the project. Try to run these commands locally (replace the /game
with the name of your repository):
PUBLIC_URL='/game' npm run build
It will produce some files in the ./build
directory. Check the source code of the build/index.html
file. All the reosurce links must be prefixed with /game
:
<link rel="icon" href="/hello/favicon.ico" />
<script defer="defer" src="/game/static/js/main.81e90d6d.js"></script>
<link href="/game/static/css/main.9d5bf3df.css" rel="stylesheet">
Webpack will automatically add the /game
prefix to the resources it produces. But, if there are some dynamically built paths in your code, the webpack won’t handle it. E.g., if you have code like this:
function getUserAvatar(username) {
return `/assets/avatars/${username}.jpg`
}
You have to add the prefix by yourself. It may look like this:
const PUBLIC_URL = process.env.PUBLIC_URL || '';
function getUserAvatar(username) {
return `${PUBLIC_URL}/assets/avatars/${username}.jpg`
}
If you use react-router, ensure you provide a proper basename
to you router:
import React from 'react';
import { createBrowserRouter } from 'react-router-dom';
const PUBLIC_URL = process.env.PUBLIC_URL || '';
export const router = createBrowserRouter([
{
path: '/',
element: <MainPage />,
},
// ... other routes
], {
// provide your PUBLIC_URL as a basename to the router,
// so it may handle all links relative to the PUBLIC_URL
basename: PUBLIC_URL,
});
How it works
- If you used create-react-app for starting your app, your app is built with react-scripts.
- react-scripts takes the
PUBLIC_URL
environment variable. - react-scripts uses webpack behind the scenes and sets the
output.publicPath
option of webpack config to the value it gets fromPUBLIC_URL
environment variable. - The
process.env.PUBLIC_URL
variable will be also available in your own code thanks to webpack.DefinePlugin.
So, if you do not use react-scripts, you have to set the publicPath
option in webpack output by yourself according to webpack documentation. Also, consider using DefinePlugin if needed.
How to check if it works
Perhaps, you run this command to build your app:
PUBLIC_URL='/game' npm run build
Now you have the ./build
directory with all the files. Copy it to a folder named as the project name, e.g. if your build it with /game
, copy it to the game
folder:
cp build game
Then run any static server:
npx serve -p 3001
Then go to http://localhost:3001/game/ and check that your app works.
Don’t forget to remove the game
directory to avoid committing it.
Set up GitHub Actions
After you have got a workable build, it’s time to set up automatic deployment.
First of all, enable deployment to GitHub Pages in the Settings of your repository. Go to the “Pages” section and select “GitHub Actions” as a source of deployment.
Next, you have to create a GitHub Workflow that builds and publishes your project.
Create a directory .github/workflows
at the root of your repository. Then create a file with any name and the .yml
extension. I prefer to name it deploy.yml
. So I create a file .github/workflows/deploy.yml.
Let’s start to describe our workflow:
name: Deploy
on:
push:
branches: [ main ]
We just named the workflow “Deploy”. The name will be used in the “Actions” tab of your project.
And we also defined, that the workflow should start on every push event to the branch called main
. If the branch you push to is called develop
, you should write develop
instead of main
. You may also define your own trigger, please read the GitHub Workflows Docs for information.
Then, you have to define the jobs to be done to deploy your project:
name: Deploy
on:
push:
branches: [ main ]
jobs:
deploy:
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
permissions:
pages: write
id-token: write
steps:
- name: checkout
uses: actions/checkout@v3
- name: setup node
uses: actions/setup-node@v3
with:
node-version: 18.x
- name: install dependencies
run: npm ci
- name: build app
run: npm run build
env:
PUBLIC_URL: /game
- name: upload artifact
uses: actions/upload-pages-artifact@v1
with:
path: ./build
- name: deploy to github pages
id: deployment
uses: actions/deploy-pages@v1
Take a closer look at the green chunks. Replace it with your values:
- Which version of nodejs do you use? Set it in the
node-version
parameter. - What package manager do you use? If you use
npm
, leave thenpm ci
command. Otherway, replacenpm ci
with the command to install dependencies, e.g.pnpm install
. - What is the command to build your application? If you are using create-react-app, leave the
npm run build
. - What the PUBLIC_URL should be? Use your repository name here. If the repository is called
my-awesome-project
, setPUBLIC_URL: /my-awesome-project
. If the repository is<owner>.github.io
, remove the line withPUBLIC_URL
completely, because it’s not needed and your app will be served onhttps://<owner>.github.io/
. - What is the directory with application artifacts? What does
npm run build
produce? If you are using create-react-app, leave the./build
directory.
That’s it. Commit this file and push it to your repository.
How it works
So, we’ve just created a GitHub workflow. We set up the trigger that runs the workflow and the actions to perform in the workflow.
The workflow has a single job called deploy
.
jobs:
deploy:
...
The job references the environment called github-pages
, which was created automatically when you set up GitHub Pages deployment source in repository settings. And set the URL of the environment to the output parameter called page_url
of the deployment
step, I will explain it a little further.
jobs:
deploy:
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
The job also has permission to write to the GitHub Pages of your repository. And write a GITHUB_TOKEN environment variable. These settings is required by the official deploy-pages action.
jobs:
deploy:
...
permissions:
pages: write
id-token: write
Then there are steps of the job, that define what to do. These two steps clone the repository and install a target nodejs version:
jobs:
deploy:
...
steps:
- name: checkout
uses: actions/checkout@v3
- name: setup node
uses: actions/setup-node@v3
with:
node-version: 18.x
These are the official actions by GitHub, you can even check its source code: actions/checkout, actions/setup-node.
The next steps install project dependencies and build the app:
jobs:
deploy:
...
steps:
...
- name: install dependencies
run: npm ci
- name: build app
run: npm run build
env:
PUBLIC_URL: /game
You can find that there is no uses
parameter there. The steps just execute the commands in the run
parameter in a shell, with the environment variables defined in the env
section. So, after running these steps, you’ll have the build artifact with your app.
The last two steps deploy the artifact to GitHub Pages hosting:
jobs:
deploy:
...
steps:
...
- name: upload artifact
uses: actions/upload-pages-artifact@v1
with:
path: ./build
- name: deploy to github pages
id: deployment
uses: actions/deploy-pages@v1
The actions/upload-pages-artifact actually just creates an archive of the folder you set in the path (./build
folder in our case) and uploads the archive to some internal GitHub storage, so other steps can fetch the archive to work with it.
And the last actions/deploy-pages takes the archive generated by upload-pages-artifact
and deploys it directly to GitHub Pages hosting. Notice the id: deployment
line in this action. Due to this id, we can reference the step in other parts, like… in the environment section:
jobs:
deploy:
...
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
...
steps:
...
- name: deploy to github pages
id: deployment
uses: actions/deploy-pages@v1
This is how we set up the environment URL to the value, that the step with id deployment
returns in its outputs.
How to check if it works
There is only way: write the .github/workflows/deploy.yml
, commit, push and wait.
In about a minute you can go to your repository “Actions” tab and find a workflow there:
You can click on the deployment and see its jobs. In our case there is single job called “deploy”. You can also see the deployment URL:
Let’s click on the URL and check if your application is working correctly! Now the app is available to the world.
Handle 404 errors with react-router
If you have client routing in your app, built with react-router or something, you may face another issue. You navigate to some page in your app, like /game/player/:playerId
, and it works perfectly. But when you refresh the page, you get the 404 error.
That’s why it happens:
- When you go to
https://<owner>.github.io/<project>/
, a browser fetchesindex.html
and other resources, likemain.js
andmain.css
. - Then, when you click on the links inside your app. Perhaps the link href is
/game/player/foo
react-router pushes a new state to browser history, and react re-renders your app. But there is no page refresh. It is why it’s called Single Page App, you know. - But then you perform a refresh of the page (or open any link in a new tab), and a browser requests a page by the address
/game/player/foo/index.html
. And there is noindex.html
file there. Your app just serves/game/index.html
and some JS and CSS files. So you get the 404 error.
There are some ways to fix it. The Create React App Documentation suggests these two:
- To use
hashHistory
, so instead of/game/player/foo
, your URLs will be like/game/#/player/foo
. - The trick with a redirect from any 404 page to the
index.html
page. It is better in the aspect of displaying URLs, they will be like/game/player/foo
. But it also causes an extra redirect: when you go to an internal URL of your app, a browser fetches404.html
, then the page redirects to theindex.html
with some parameters, and thenindex.html
renders the app. This option also requires you to inject some scripts into yourindex.html
file to handle the redirect.
But I prefer the third way. If you add the 404.html
file to your website, GitHub Pages will show this page for any 404 error. But we want to render our react app there. What if we make the 404.html
totally the same as the index.html
file? It will work:
- If the browser requests some URL like
/game/player/foo
, the GitHub Pages answer with the content of404.html
. - The
404.html
includesmain.js
andmain.css
files, so the app will be rendered and work as expected. - If we go to a different route, it will be captured by react-router and work.
- If we refresh the page or open some links in a new tab, it will be captured by the
404.html
again.
So, the only thing we need to do is copy build/index.html
to build/404.html
before deploying the app. I prefer to do it in the following steps:
- Add a script that copies the file in the package.json:
"scripts": { "start": "react-scripts start", "build": "react-scripts build", ... "gh-pages:404": "cp build/index.html build/404.html" }
- Run the scripts just after the building part in the deploy.yml:
jobs: deploy: ... steps: ... - name: build app run: npm run build && npm run gh-pages:404 env: PUBLIC_URL: /game ...
Then commit and push the changes in package.json
and deploy.yml
and wait for the deployment finish.
Now your app is perfectly deployed!
You can find all source code in one place at github.com/isqua/demo-routing-in-typescript-react-app.