GitHub Pages Deployments That Stop Being Confusing
GitHub Pages feels confusing for one reason: base paths. Once you understand that your site does not live at / but at /your-repo/, every broken deployment you’ve ever seen suddenly makes sense. This guide is start to finish. It uses real commands, real files, and real verification steps. No shortcuts. No magic. When you finish, you will have a live URL that works, refreshes correctly, and serves assets without 404s.
This is not a theory post. It is a lab you can run in any static site project.
The promise
By the end, three things will be true. Your project will build into a static folder. That folder will be published to a gh-pages branch. The live GitHub Pages URL will load with working CSS and JavaScript. If any of those are missing, the setup is incomplete.
What GitHub Pages actually is
At a high level, GitHub Pages is just a static file host wired directly into your repository. You push a folder of HTML, CSS, and JavaScript to a specific branch, and GitHub serves it at a predictable URL. No servers. No containers. No runtime. Just files on disk served over HTTP.
At a low level, GitHub Pages does exactly three things. It watches a branch. It copies the files from that branch to its hosting layer. It assigns your repo a URL. Everything else people get stuck on is configuration around paths and build output.
The only term that matters is the base path. Your site will live at:
https://yourname.github.io/your-repo/
If your app thinks it lives at /, every asset will point to the wrong place in production. That is the entire source of the confusion.
What you need before touching anything
You need a GitHub repository. You need a build command that outputs a static folder. You need to be able to run that build locally without errors. If your local build is broken, GitHub Pages will faithfully publish a broken build.
Nothing else is required. No paid services. No API keys. No secret configs.
Start to finish
Step 1: Set the base path
The first fix is always the same. You tell your project what URL it will live under in production. In most JavaScript static site setups, that is done with a homepage field in package.json.
File:
package.json
Add or confirm:
{"homepage": "https://yourname.github.io/your-repo"}
This one line changes how the build outputs asset URLs. Without it, assets point to /assets/.... With it, assets point to /your-repo/assets/.... That is the difference between a working site and a blank page with console errors.
Verification:
Run a local build:
npm run build
Open the generated index.html inside the build folder. Inspect the <script> and <link> tags. If you see /your-repo/ in the paths, the base path is correct. If you still see /, the value is wrong or the build tool did not pick it up.
If this step is skipped or wrong, every later step will technically succeed but the deployed site will be broken. Fix the base path first. Always.
Step 2: Add a deploy script
Once the build folder is correct, you need a repeatable way to publish it. The simplest approach is the gh-pages npm package. It pushes a local folder to a remote gh-pages branch in your repository.
Install it:
npm install -D gh-pages
Now wire a deploy command.
File:
package.json
Add:
{"scripts": {"deploy": "gh-pages -d dist"}}
Replace dist with whatever folder your build actually outputs. Some tools use build, some use public. The command must point to the folder that contains index.html.
This script does one thing. It takes your local build output and pushes it to a branch named gh-pages. That branch is what GitHub Pages will serve.
Verification:
Run:
npm run deploy
Expected result: a new branch named gh-pages appears in your GitHub repository. If it does, the publishing step worked.
If it fails, the error will almost always say the folder does not exist. That means the build output folder name in your script does not match your actual build.
Step 3: Turn on GitHub Pages
Publishing the branch is not enough. You still need to tell GitHub to serve that branch.
Open your repository on GitHub. Go to Settings → Pages. Under Source, select:
- Deploy from branch
- Branch:
gh-pages - Folder:
/
Save. GitHub will now build and host your branch. After a short delay, it will show you the live URL.
Verification:
Open the URL GitHub provides. The page should load with styling and JavaScript working. Open DevTools → Network. There should be no 404s for CSS or JS files.
If you see a blank page or console errors, go back to Step 1. It is almost always the base path.
Deploying a plain HTML page with GitHub Actions (no gh-pages branch)
The gh-pages branch approach works, but it is not the only way, and it is not the cleanest way anymore. GitHub Pages can also be deployed directly from GitHub Actions. The mental model is different.
With Actions-based Pages deployments, you do not push files to a gh-pages branch. Instead, your workflow builds (or just gathers) a folder of static files, uploads it as a Pages artifact, and GitHub publishes that artifact to your Pages URL. Your repository stays cleaner because your generated site output does not live in your git history.
This section is a complete lab for a dead simple static site. It uses a single index.html, an optional styles.css, and a GitHub Actions workflow that deploys on every push to main.
Step 0: Make sure Pages is set to “GitHub Actions”
Open your repository on GitHub. Go to Settings → Pages. Under “Build and deployment,” set the Source to GitHub Actions. If you leave it on “Deploy from a branch,” the workflow can run perfectly and you will still sit there wondering why nothing updates.
Step 1: Create the simplest possible site
In a new repo, create a folder and add index.html. This example is intentionally boring because the point is deployment mechanics, not design.
mkdir gh-pages-html-democd gh-pages-html-demogit initgit branch -M maingit config user.name "Your Name"git config user.email "you@example.com"cat > index.html <<'EOF'<!doctype html><html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>GitHub Pages HTML Demo</title><link rel="stylesheet" href="styles.css" /></head><body><main class="wrap"><h1>GitHub Pages via GitHub Actions</h1><p>This page is deployed from a workflow, not a gh-pages branch.</p><p id="stamp"></p></main><script>document.getElementById("stamp").textContent = "Deployed at: " + new Date().toISOString();</script></body></html>EOFcat > styles.css <<'EOF':root { color-scheme: light dark; }body { margin: 0; font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; }.wrap { max-width: 72ch; margin: 0 auto; padding: 3rem 1.25rem; }h1 { line-height: 1.1; margin: 0 0 1rem; }p { line-height: 1.6; margin: 0 0 0.9rem; }EOFgit add .git commit -m "Add simple static page"
At this point you have a real static site. There is no build step. The folder itself is the deployable artifact.
Step 2: Add the GitHub Actions workflow
Create this file:
.github/workflows/pages.yml
name: Deploy Pageson:push:branches: ["main"]permissions:contents: readpages: writeid-token: writeconcurrency:group: "pages"cancel-in-progress: truejobs:deploy:environment:name: github-pagesurl: ${{ steps.deployment.outputs.page_url }}runs-on: ubuntu-lateststeps:- name: Checkoutuses: actions/checkout@v4- name: Setup Pagesuses: actions/configure-pages@v5- name: Upload artifactuses: actions/upload-pages-artifact@v3with:path: .- name: Deployid: deploymentuses: actions/deploy-pages@v4
This workflow is doing four real things.
Checkout pulls your repo onto the runner. Configure Pages sets up the Pages deployment context. Upload Pages Artifact packages a folder into the special artifact GitHub Pages expects. Deploy Pages publishes that artifact and returns the live URL as an output.
The permissions block matters. Pages deployments require pages: write and id-token: write. If you omit them, the workflow can fail with confusing authorization errors even though the YAML “looks right.”
The path: . line is intentionally blunt for this demo. It uploads the repository root, which contains index.html and styles.css. In a real app, you would upload the build output directory, like dist or build.
Step 3: Push the repo to GitHub
Create the repo on GitHub, add the remote, then push.
git remote add origin https://github.com/<yourname>/gh-pages-html-demo.gitgit push -u origin main
As soon as the push finishes, the workflow will run. Go to the Actions tab and click the latest run. When it finishes, it will show the deployed Pages URL.
Verification: prove it updated
Open the deployed URL and refresh twice. You should see the timestamp change. That is your proof that you are not looking at an old cached build and that the workflow is really deploying the current repo contents.
If the page loads but CSS is missing, open DevTools → Network and look for a 404 on styles.css. In this plain HTML demo, that almost always means you uploaded the wrong folder, not that Pages is broken.
Deploying a built site with Actions (dist/build/public)
Most real projects produce a build folder. The only change you make is what you upload.
Instead of uploading path: ., you run your build and upload the build output directory.
Example shape:
- name: Installrun: npm ci- name: Buildrun: npm run build- name: Upload artifactuses: actions/upload-pages-artifact@v3with:path: dist
The rest of the workflow stays the same. The only strict rule is that the uploaded folder must contain an index.html at its root.
Common Actions-based Pages failures
| Symptom | What it usually means | Fix |
|---|---|---|
| Workflow succeeds but Pages URL never changes | Pages Source is still set to branch deploy | Settings → Pages → Source → GitHub Actions |
| Deploy step fails with permission/authorization errors | Missing Pages permissions | Add pages: write and id-token: write |
| Page loads but assets 404 | Wrong upload folder or wrong base path | Upload the correct build folder and re-check base paths |
| Build works locally but fails in Actions | Your local environment is hiding an assumption | Pin Node version and make build deterministic |
This is still the same story as the branch method. Pages is simple. Your paths and your build output are what make it feel complex.
Verify it worked
A correct deployment has three properties. The root URL loads. Refreshing the page does not break routing. Assets load without 404 errors. If those are true, your GitHub Pages deployment is complete.
Common failure patterns
A blank page with red console errors is a wrong base path. A 404 at the Pages URL is a branch or settings issue. An old version showing after a successful deploy is browser caching. None of these require guesswork. Each maps directly to one of the steps above.
Why this feels harder than it is
GitHub Pages itself is simple. The confusion comes from modern front-end tools defaulting to / as the base path, while GitHub Pages requires /repo-name/. Once you align those two, the rest is mechanical.
You are not configuring a server. You are not managing infrastructure. You are just building files and telling GitHub which branch to serve. That’s all that’s happening.
Closing
If you set the base path, build locally, push the build folder to gh-pages, and enable Pages in repo settings, your deployment will work. Every time. If it doesn’t, one of those four steps is wrong, not GitHub Pages itself.