GitHub Pages serves static files — HTML, CSS, JavaScript, images. It cannot run a backend, but it also cannot run a build step automatically. For projects like Excalidraw or any React/Vue/Svelte app, the raw repository contains source code (JSX, TypeScript) that browsers can’t read directly. The code has to be compiled first.
The fix is to use GitHub Actions to build the code on every commit, then push the compiled output to GitHub Pages.
The general pattern
There are two common variants depending on how the upstream project is structured:
Variant 1: Build to a gh-pages branch (older pattern)
Many older React projects use the gh-pages npm package or a similar tool. They commit source to main/master, then publish the built artifacts to a separate gh-pages branch. GitHub Pages is configured to serve from that branch.
A typical GitHub Action does:
- Checkout the source from
main npm install/yarn install- Run the build (
npm run buildor similar) - Push the build output to a
gh-pagesbranch
Once the action completes, the user goes to Settings → Pages and sets the Source to “Deploy from a branch” → gh-pages → / (root).
Variant 2: Build and deploy via GitHub Pages Actions (newer pattern)
GitHub Pages now has first-class support for deploying directly from an Action without needing a gh-pages branch. The workflow uses actions/configure-pages, actions/upload-pages-artifact, and actions/deploy-pages.
A minimal workflow:
name: Deploy to GitHub Pages
on:
push:
branches: [master]
permissions:
contents: read
pages: write
id-token: write
jobs:
build-and-deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
- name: Install dependencies
run: yarn install
- name: Build
run: yarn build
- name: Setup GitHub Pages
uses: actions/configure-pages@v4
- name: Upload build output
uses: actions/upload-pages-artifact@v3
with:
path: './dist'
- name: Deploy
id: deployment
uses: actions/deploy-pages@v4Adjust path: to wherever the framework outputs the build (usually dist, build, or excalidraw-app/build for the Excalidraw repo specifically).
Common gotchas
The homepage field in package.json
Many React projects set a homepage URL in package.json so paths resolve correctly. If you fork a repo, the original homepage will be wrong:
"homepage": "https://originalauthor.github.io/excalidraw/"Change it to:
"homepage": "https://yourusername.github.io/your-repo-name/"Without this, the deployed site often shows a blank page because asset URLs resolve to /static/... paths that don’t exist on the deployed domain.
Workflow permissions
By default, GitHub restricts Actions from creating new branches. To allow a gh-pages push:
- Repository Settings → Actions → General
- Scroll to “Workflow permissions”
- Switch to “Read and write permissions”
- Save
Path to build output
Different frameworks output to different folders:
- Create React App:
build/ - Vite:
dist/ - Next.js (static export):
out/ - Excalidraw:
excalidraw-app/build/ - Webpack default:
dist/
If the deploy step fails or shows an empty site, this is almost always why.
Where it breaks down
GitHub Pages is excellent for static apps. It is not a fit for:
- Apps requiring a backend
- Apps with server-side rendering (use Vercel/Netlify instead)
- Apps using features that require server-side cookies or sessions
For the Zapp pattern of fully client-side apps, GitHub Pages is essentially the perfect free host.