How To Deploy a Static Site to GitHub Pages With gh-pages and GitHub Actions (2026 Update)
This tutorial will show you how to use gh-pages and GitHub Actions to automatically deploy a static site (Astro, Gatsby, Next, etc.) to GitHub Pages. These steps and scripts are currently in use on this site.
Initial Context
This guide assumes there are two git repos, one to house the static site project code and one for the GitHub Pages public files. Each git repo will have these characteristics:
- Static site project repo
- GitHub Pages repo
- files in repo were generated via Gatsby project
- public web root for GitHub Pages
- NOTE: For this tutorial, the git repo
https://github.com/username/username.github.io.gitis assumed
Setting up GitHub Actions secret
In order to transfer files from one GitHub repo to another, the GitHub Actions need an authentication token to commit to the correct repo. The recommended approach in 2026 is a fine-grained personal access token. Follow these steps to set up a secret.
- Create a fine-grained personal access token
- https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens
- Scope the token to only the GitHub Pages repo (not the source repo)
- Required permission:
Contents: Read and Write - Fine-grained tokens are repo-scoped and more secure than classic tokens
- For the project repo, store the token as a GitHub Actions secret
- https://docs.github.com/en/actions/security-guides/using-secrets-in-github-actions
- NOTE: In this tutorial,
DEPLOY_TOKENis used as the key
This secret will be used in both the deploy script and in the GitHub Actions workflow.
Installing npm packages
For the deploy step, one npm package is needed:
- Install gh-pages as a dev dependency
- Run
npm install --save-dev gh-pages
- Run
Setup Deployment
Now for the fun part! The next two scripts will work in tandem to deploy the Gatsby project to the Github Pages repo.
Making the deploy script
Add this npm script to the project’s package.json.
"scripts": {
...
"build": "astro build",
"deploy": "gh-pages -d dist -b main --dotfiles --nojekyll -r https://$DEPLOY_TOKEN@github.com/username/username.github.io.git"
}
astro build(or your framework’s build command) will build the site into thedistdirectorygh-pageswill deploy the site with these flag arguments-d distis the directory containing the built site-b mainis the branch name of the destination repo--dotfilesincludes dotfiles (like.nojekyll) in the push--nojekyllcreates a.nojekyllfile to disable GitHub Pages Jekyll processing (required for static site generators)-r https://$DEPLOY_TOKEN@github.com/username/username.github.io.gitis the destination repo with the authentication token embedded
Setting up the GitHub Actions workflow
Add this yaml file to the project’s .github/workflows directory. Name it deploy.yaml, resulting in .github/workflows/deploy.yaml.
name: Deploy site
on:
push:
branches: [main]
jobs:
deploy-site:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: '24'
cache: 'npm'
- run: npm install --legacy-peer-deps
- run: npm run lint
- run: npm run build
- run: npm test
- name: Deploy site
run: |
npx gh-pages -d dist -b main --dotfiles --nojekyll \
-m "deploy site via GitHub Actions" \
-u "YourName <your-email@example.com>" \
-r "https://$DEPLOY_TOKEN@github.com/username/username.github.io.git"
env:
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
- Triggers on any
git pushto themainbranch actions/checkout@v6— latest version running on Node 24actions/setup-node@v6— latest version running on Node 24node-version: '24'installs Node.js 24cache: 'npm'cachesnode_modulesfor faster installs--legacy-peer-depsbypasses strict peer dependency checks (needed for some projects with legacy ESLint configs)
npm run lint— check code quality before buildingnpm run build— build the static site intodist/npm test— run tests against the built output (tests that read fromdist/require the build to run first)- Deploy step runs
gh-pagesdirectly with:--dotfiles— include dotfiles (ensures.nojekyllis pushed)--nojekyll— explicitly create.nojekyllon the target branch-u— set the commit author metadata$DEPLOY_TOKEN— the fine-grained PAT passed as an environment variable
2026 Updates
This post was originally written in 2022 and updated in 2026. Here’s what changed:
Node.js 24 and GitHub Actions Deprecations
- Node 24 is now the default on GitHub Actions runners. Node 20 reached end-of-life in April 2026 and was removed from runners in September 2026.
- Action versions were updated:
actions/checkout@v6andactions/setup-node@v6now bundle Node 24 as their runtime. Older versions using Node 20 will stop working. - Use
node-version: '24'to match your.nvmrcand local development environment.
Fine-Grained Personal Access Tokens
- Classic PATs are being phased out. Use fine-grained PATs scoped to a single repository.
- Name the secret clearly:
DEPLOY_TOKENinstead ofPERSONAL_ACCESS_TOKEN. - The token only needs
Contents: Read and Writeon the GitHub Pages repo — nothing else.
Build Before Test
- Tests that validate the build output (e.g., checking HTML, RSS, sitemap files) need
dist/to exist. Runnpm run buildbeforenpm test. - The deploy step runs
gh-pagesdirectly instead of via the npm script to avoid a second build.
gh-pages and .nojekyll
- gh-pages does not add
.nojekyllby default. Pass--nojekyllto create it, and--dotfilesto ensure dotfiles from the source are included. - Without
.nojekyll, GitHub Pages runs Jekyll processing which can break static sites.
Conclusion
That’s it! Any push to the project repo will trigger a lint → build → test → deploy pipeline. Assuming username.github.io was the repo name and GitHub Pages was configured, there should be a live site at https://username.github.io!