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

Deploying static site Debugging process checkpoint illustration for this section.

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.

Inspecting built assets

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.

Pushing to gh-pages

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.

Live site online

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.

GitHub Actions running

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-demo
cd gh-pages-html-demo
git init
git branch -M main
git 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>
EOF
cat > 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; }
EOF
git 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 Pages
on:
push:
branches: ["main"]
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: "pages"
cancel-in-progress: true
jobs:
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Pages
uses: actions/configure-pages@v5
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: .
- name: Deploy
id: deployment
uses: 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.

Deploy Success

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.git
git 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: Install
run: npm ci
- name: Build
run: npm run build
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
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

SymptomLikely causeFix
Workflow passes, site does not updatePages source still set to “Deploy from a branch”Go to Settings → Pages → Source → GitHub Actions
Error: Could not retrieve tokenMissing id-token: writeAdd it under permissions
Error: Failed to create deploymentMissing pages: writeAdd it under permissions
404 on CSS or JS after deploypath points to the wrong folderConfirm it matches your build output
Page stays blankBase path mismatchSet homepage or equivalent for your build tool
Inspecting workflow logs Verification and access checkpoint illustration for this section.

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.

Keep reading