This is a lightweight solution for deploying single page apps with GitHub Pages. You can easily deploy a React single page app with React Router <BrowserRouter />
, like the one in the demo app, or a single page app built with any frontend library or framework.
GitHub Pages doesn't natively support single page apps. When there is a fresh page load for a url like example.tld/foo
, where /foo
is a frontend route, the GitHub Pages server returns 404 because it knows nothing of /foo
.
When the GitHub Pages server gets a request for a path defined with frontend routes, e.g. example.tld/foo
, it returns a custom 404.html
page. The custom 404.html
page contains a script that takes the current url and converts the path and query string into just a query string, and then redirects the browser to the new url with only a query string and hash fragment. For example, example.tld/one/two?a=b&c=d#qwe
, becomes example.tld/?/one/two&a=b~and~c=d#qwe
.
The GitHub Pages server receives the new request, e.g. example.tld/?/...
, ignores the query string and returns the index.html
file, which has a script that checks for a redirect in the query string before the single page app is loaded. If a redirect is present it is converted back into the correct url and added to the browser's history with window.history.replaceState(...)
, but the browser won't attempt to load the new url. When the single page app is loaded further down in the index.html
file, the correct url will be waiting in the browser's history for the single page app to route accordingly. (Note that these redirects are only needed with fresh page loads, and not when navigating within the single page app once it's loaded).
For general information on using GitHub Pages please see Getting Started with GitHub Pages, note that pages can be User, Organization or Project Pages
Basic instructions - there are two things you need from this repo for your single page app to run on GitHub Pages.
404.html
file to your repo as is
username.github.io/repo-name
), then you need to set pathSegmentsToKeep
to 1
in the 404.html
file in order to keep /repo-name
in the path after the redirect. If you are using React Router you'll need to tell it to use the repo-name
as the basename
, for example <BrowserRouter basename="/repo-name" />
.index.html
file and add it to your index.html
file - Note that the redirect script must be placed before your single page app script in your index.html
file.
Detailed instructions - using this repo as a boilerplate for a React single page app hosted with GitHub Pages. Note that this boilerplate is written in TypeScript but is setup to accept JavaScript files as well. It was previously written in JS and if you prefer a JS only boilerplate you can use version 6.
$ git clone https://github.com/rafgraph/spa-github-pages.git
).git
directory (cd
into the spa-github-pages
directory and run $ rm -rf .git
)$ git init
in the spa-github-pages
directory, and then $ git add .
and $ git commit -m "Add SPA for GitHub Pages boilerplate"
to initialize a fresh repositorymain
to gh-pages
($ git branch -m gh-pages
), if this will be a User or Organization Pages site, then leave the branch name as main
$ git remote add origin <your-new-github-repo-url>
)spa-github-pages
directory to anything you want (e.g. your-project-name
)gh-pages
branch of an existing repository
gh-pages
for your existing repo ($ git checkout --orphan gh-pages
), note that the gh-pages
branch won't appear in the list of branches until you make your first commit.git
directory) from the directory of your existing repo ($ git rm -rf .
)spa-github-pages
directory into your project's now empty directory ($ mv path/to/spa-github-pages/{.[!.],}* path/to/your-projects-directory
)$ git add .
and $ git commit -m "Add SPA for GitHub Pages boilerplate"
to instantiate the gh-pages
branchCNAME
file with your custom domain, don't include https://
, but do include a subdomain if desired, e.g. www
or your-subdomain
CNAME
and/or A
record with your DNS provider$ dig your-subdomain.your-domain.tld
to make sure it's set up properly with your DNS (don't include https://
)CNAME
fileusername.github.io/repo-name
):
pathSegmentsToKeep
to 1
in the 404.html
file in order to keep /repo-name
in the path after the redirectrepo-name
to the absolute path of assets in index.html
, change the bundle.js src to "/repo-name/build/bundle.js"
basename
to /repo-name
here like <BrowserRouter basename="/repo-name" />
package.json
replace --open
with --open-page repo-name
webpack.config.js
:repo-name
to the publicPath
like publicPath: '/repo-name/build/'
historyApiFallback rewrites
to rewrites: [{ from: /\/repo-name\/[^?]/, to: '/404.html' }]
$ npm install
to install React and other dependencies, and then run $ npm run build
to update the build$ git add .
and $ git commit -m "Update boilerplate for use with my domain"
and then push to GitHub ($ git push origin gh-pages
for Project Pages or $ git push origin main
for User or Organization Pages) - the example site should now be live on your domainindex.html
and the title in 404.html
to your site's titleindex.html
and the favicon
directory.robots.txt
and sitemap.txt
as you see fit (see SEO section below for more info)$ npm run build
(this runs webpack -p
for production) to update the build, then $ git commit
and $ git push
to make your changes liveServing from the /docs
folder on the main
branch - alternatively you can serve your site from the /docs
folder instead of the root folder while your source code remains in the root folder.
/docs
folder in the root and move index.html
, 404.html
and the /build
folder into /docs
--content-base docs/
to the start script in package.json
webpack.config.js
change the output path to path: `${__dirname}/docs/build`,
/docs
folder as the source for GitHub PagesI have included webpack-dev-server
for testing changes locally. It can be accessed by running $ npm start
(details below). Note that webpack-dev-server
automatically creates a new bundle whenever the source files change and serves the bundle from memory, so you'll never see the bundle as a file saved to disk.
$ npm start
runs the start script in package.json
, which runs the command $ webpack-dev-server --host 0.0.0.0 --disable-host-check --open
--host 0.0.0.0 --disable-host-check
is so you can access the site on your local network from other devices at http://[YOUR COMPUTER'S IP ADDRESS]:8080
--open
will open automatically open the site in your browserwebpack-dev-server
will serve index.html
at http://localhost:8080
(port 8080
is the default). Note that you must load the index.html
from the server and not just open it directly in the browser or the scripts won't load.When I first created this solution in 2016 Google treated the redirect in 404.html
the same as a 301 redirect and indexed pages without issue. Around 2019 Google changed their algorithm and no longer follows redirects in 404.html
. In order to have all the pages on your site indexed by Google you need to create a robots.txt
and sitemap.txt
file to let Google know what pages exist. The robots.txt
file needs to contain the location of the sitemap, and the sitemap.txt
file needs to contain the redirect links for each page of your site so the crawler doesn't get a 404 response when it requests the page. To make this easier I created a sitemap link generator that transforms normal links into redirect links to use in the sitemap. I have done this for the demo site (this repo) and you can see the pages indexed here. Note that since Google is no longer associating the redirect links with the real paths, incoming links from other sites won't help your site's page rank. If you are creating a site where page rank on generic search terms is important, then I'd suggest looking for another solution. Some options are using GitHub Pages with a static site generator like Gatsby which generates an html
file for each page as part of its build process, or hosting your single page app on a service that has native support for spas, like Netlify.
.nojekyll
file in this repo turns off Jekyll for GitHub Pages