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.

Deploying static site

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.

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.

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.

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 (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: 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

SymptomWhat it usually meansFix
Workflow succeeds but Pages URL never changesPages Source is still set to branch deploySettings → Pages → Source → GitHub Actions
Deploy step fails with permission/authorization errorsMissing Pages permissionsAdd pages: write and id-token: write
Page loads but assets 404Wrong upload folder or wrong base pathUpload the correct build folder and re-check base paths
Build works locally but fails in ActionsYour local environment is hiding an assumptionPin 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.