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 have 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.
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.
How to deploy from a branch
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.
How to deploy with GitHub Actions instead
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
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 | Likely cause | Fix |
|---|---|---|
| Workflow passes, site does not update | Pages source still set to “Deploy from a branch” | Go to Settings → Pages → Source → GitHub Actions |
Error: Could not retrieve token | Missing id-token: write | Add it under permissions |
Error: Failed to create deployment | Missing pages: write | Add it under permissions |
| 404 on CSS or JS after deploy | path points to the wrong folder | Confirm it matches your build output |
| Page stays blank | Base path mismatch | Set homepage or equivalent for your build tool |
Closing
GitHub Pages is not complicated once you respect the base path and the deployment source. Whether you use a gh-pages branch or a GitHub Actions workflow, the same rule applies: your build output must know where it will live, and you must confirm the live URL actually serves the files you expect.
If you take one thing from this post, take the verification step. Open DevTools. Check for 404s. Refresh until the timestamp proves you are looking at the new build. That single habit will save you hours of confusion.