GitHub Pages.
Your site on the real internet, for free, in ten minutes.
GitHub Pages turns a repository into a website: every push to your repo automatically redeploys the site to a global CDN, with free HTTPS, at username.github.io — or your own domain. No servers, no billing page, no credit card field anywhere in the flow. If you finished the Git field guide, you already know 90% of what this takes; the last 10% is a handful of clicks in GitHub's Settings.
git push
your Mac
Pages build
GitHub Actions
Live on CDN
github.io
What "completely free" actually includes
GitHub Pages is genuinely free — not trial-free, not freemium-free. On GitHub's Free plan you get, per account:
- Hosting for unlimited public repositories, each of which can be a website. (Pages on private repos requires a paid plan — so keep the site's repo public, which is normal for a personal site anyway.)
- A free subdomain:
username.github.io, plususername.github.io/repo-namefor every additional project. - Free HTTPS — automatically provisioned certificates, on the github.io address and on custom domains alike.
- A global CDN (Fastly) serving your files from edge locations worldwide.
- Automatic redeployment on every push — the pipeline animated above.
The fine-print limits are generous for any personal or project site: sites should stay under 1 GB, individual files under 100 MB, bandwidth has a soft limit of 100 GB/month, and builds are capped at 10 per hour. The one hard rule that matters: Pages serves static content only — HTML, CSS, JavaScript, images, fonts. No server-side code runs there: no PHP, no databases, and — relevant to your roadmap — no Perl CGI. Your Apache setup at mysite.test remains the right home for the CGI experiments; Pages is the right home for the static site the world sees.
"Static" is less limiting than it sounds. JavaScript still runs in the visitor's browser, so interactive pages — like the HTML guides you build, with their canvas demos and IntersectionObserver navigation — work perfectly. Only server-side execution is off the table.
One decision first: user site or project site
Pages offers two flavors, and the only difference in setup is the repository's name:
| User site | Project site | |
|---|---|---|
| Repo must be named | username.github.io exactly | anything — mysite, french-grammar, … |
| Site URL | https://username.github.io/ | https://username.github.io/mysite/ |
| How many allowed | one per account | unlimited |
| Best for | your main homepage / portfolio | each project, guide collection, experiment |
They can coexist: your user site at the root, and any number of project sites hanging off it. Not sure which you're making? Answer below and the rest of the guide's placeholders resolve themselves:
Planner · Fill in your details
interactivePath A — entirely in the browser (no Git needed)
The fastest possible route, good for a first taste or for someone without a terminal handy. Everything happens on github.com:
Create the account
Sign up at github.com — the free plan is the default; you're never asked for payment. Choose the username carefully: it becomes your site's address,
username.github.io, forever visible in the URL bar.Create the repository
Click the + in the top-right → New repository. For your homepage, name it exactly
username.github.io(substituting your real username — GitHub even hints when you get this right). Set it to Public, tick "Add a README file" so the repo isn't empty, and click Create repository.Add your index.html
In the repo, click Add file → Create new file. Name it
index.html— this exact name is what Pages serves when someone visits your root URL. Paste your HTML (a minimal starter is below), then click Commit changes. You just made a Git commit without touching Git.Turn on Pages
Covered in full in Step 05 — it's the same two clicks for both paths.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hello from GitHub Pages</title>
</head>
<body>
<h1>It's alive.</h1>
<p>Served free, over HTTPS, from a global CDN.</p>
</body>
</html>You can keep working this way indefinitely — GitHub's web editor (press . in any repo for a full VS Code in the browser) is surprisingly capable. But the moment you have a real project locally, Path B is better in every way.
Path B — push your existing site from the Mac
This is the grown-up route, and it's exactly the remote workflow from the Git guide. Suppose the site lives in ~/Sites/mysite and is already a Git repository (if not: git init, git add ., git commit -m "Initial commit" — thirty seconds).
Authenticate once
GitHub doesn't accept account passwords from Git. The two clean options — pick one:
option 1 · ssh key (classic, no extra tools)ssh-keygen -t ed25519 -C "you@example.com" # accept defaults pbcopy < ~/.ssh/id_ed25519.pub # copies the PUBLIC key # github.com → Settings → SSH and GPG keys → New SSH key → paste ssh -T git@github.com # test: "Hi username!"option 2 · github cli (easiest)brew install gh gh auth login # choose GitHub.com → HTTPS → login via browserCreate the empty repo on GitHub
On github.com: + → New repository, named
username.github.io(user site) or e.g.mysite(project site). Public. This time add nothing — no README, no .gitignore — so the remote starts empty and your local history pushes in cleanly. (With the CLI it's one line:gh repo create username.github.io --public --source=. --pushdoes this step and the next two.)Connect local ↔ remote
in ~/Sites/mysitegit remote add origin git@github.com:USERNAME/USERNAME.github.io.git git remote -v # verify — shows fetch & push URLsPush
first pushgit push -u origin main Enumerating objects: 12, done. Writing objects: 100% (12/12), 24.3 KiB | 8.1 MiB/s, done. To github.com:USERNAME/USERNAME.github.io.git * [new branch] main -> mainRefresh the repo page on GitHub — your files are there, full history intact. One step remains.
Make sure index.html sits at the repository root (or in a /docs folder — see the next step), not nested in some subfolder like mysite/site/html/. Pages serves the chosen folder as-is: root of folder = root of website.
Turning Pages on: the two clicks that matter
This is the entire "hosting setup." In your repository on github.com:
Source: "Deploy from a branch"
Under Build and deployment → Source, keep Deploy from a branch. (The alternative, GitHub Actions, is for sites that need a build step — Jekyll with plugins, Hugo, a bundler. Plain HTML/CSS/JS doesn't.)
Branch:
main, folder:/ (root)Select branch main and folder / (root), then click Save. The folder dropdown also offers
/docs— useful when you want the website in a subfolder of a larger repo (code at root, site indocs/).That's it — wait ~1 minute
GitHub kicks off the first deploy immediately. Refresh the Pages settings page and a banner appears: "Your site is live at https://username.github.io/" with a Visit site button. First deploys can take a couple of minutes; subsequent ones are usually 20–60 seconds.
404 after it says "live"? Ninety percent of the time it's one of two things: the file isn't named exactly index.html (lowercase, no Index.html or index.HTML), or it isn't at the root of the folder you selected. The other ten percent: give the CDN two more minutes and hard-refresh (⌘⇧R).
Watching the deploy (and knowing when it's done)
Every deploy — including that first one — runs as a visible workflow. Two places to watch:
- The Actions tab of your repo lists a workflow called pages-build-deployment for every push: amber dot = running, green check = live, red X = failed (click it for logs).
- The repo homepage shows the same status as a tiny icon next to the latest commit's hash.
gh run list --limit 3
STATUS NAME BRANCH EVENT
✓ pages build and deploy… main dynamic
gh run watch # live-follows the current deploy to completion
gh browse # opens the repo; add --settings for SettingsWhen the check turns green, the site is live at your URL. Verify the essentials once: pages load over https://, internal links work, images appear. Then check Settings → Pages and tick "Enforce HTTPS" if it isn't already (it usually is) — this redirects any http:// visitor to the secure version.
Updating the site: the loop you already know
From now on, "deploying" is not a separate skill. It's the Git loop with one extra word:
# edit files locally, preview in the browser, then:
git add .
git commit -m "Add Spanish grammar chart page"
git push
# ~30 seconds later the live site reflects it. That's the whole deploy.
# with the aliases from the Git guide, a full publish is:
git aa && git cm "Update charts" && git puNotice what you get free that FTP-era hosting never gave you: every version of the live site ever published is a commit you can inspect, diff, and — if a deploy breaks something — instantly roll back:
git revert HEAD # new commit that undoes the last one
git push # site redeploys to the previous stateLocal preview stays local. Your Apache at mysite.test is now the staging server: preview there, and only git push when it looks right. Two hosts, one folder, zero conflict.
Optional: your own domain (still free on GitHub's side)
The only money in this entire guide is optional: a domain name (~$10–15/year from a registrar like Cloudflare, Porkbun, or Namecheap). GitHub's side — hosting, HTTPS certificate for the domain — costs nothing. Two halves to configure:
Half 1 · at your DNS provider
Create these records (the four A records are GitHub's permanent Pages addresses):
| Type | Name / Host | Value |
|---|---|---|
A | @ (apex, i.e. example.com) | 185.199.108.153 |
A | @ | 185.199.109.153 |
A | @ | 185.199.110.153 |
A | @ | 185.199.111.153 |
CNAME | www | username.github.io |
Half 2 · in GitHub
Enter the domain
Settings → Pages → Custom domain: type
www.example.com(or the apexexample.com), click Save. GitHub adds aCNAMEfile to your repo and starts a DNS check.Wait for the check, then enforce HTTPS
DNS propagation takes minutes to a few hours. When the check shows green, tick Enforce HTTPS. GitHub provisions a Let's Encrypt certificate for your domain automatically — the checkbox may be greyed for up to a day while that happens; it resolves on its own.
(Recommended) verify the domain account-wide
Account Settings → Pages → Add a domain lets you verify ownership via a TXT record, which prevents anyone from claiming your domain on Pages if you ever unlink it.
Or generate your exact records here:
Generator · DNS records for your domain
interactivePolish & the gotchas worth knowing
The project-site path gotcha (the #1 trap)
A project site lives at username.github.io/mysite/ — note the subpath. Any link or asset URL starting with / points at the domain root, above your site, and breaks:
<!-- BREAKS on a project site: resolves to username.github.io/style.css -->
<link rel="stylesheet" href="/style.css">
<!-- WORKS everywhere: relative to the current page -->
<link rel="stylesheet" href="style.css">
<a href="charts/french.html">French charts</a>
<img src="img/hero.png">User sites (username.github.io) sit at the domain root, so both styles work there — but relative paths are the habit that never bites.
A custom 404 page
Drop a 404.html at the repo root and Pages serves it for any missing URL — same design language as your site instead of GitHub's default.
The .nojekyll file
By default, Pages runs your files through Jekyll, a static-site generator. For plain HTML it's nearly transparent — except it silently skips any file or folder starting with an underscore (_drafts/, _data.js). Opt out with an empty marker file and Pages serves everything verbatim, slightly faster:
touch .nojekyll
git add .nojekyll && git commit -m "Serve files verbatim" && git pushThings Pages will never do (plan around them)
- No server-side code — for form handling use a service (Formspree et al.) or a separate backend; for Perl CGI, that's your Apache box.
- No secrets in the repo — the repository is public; so is every file and its entire history.
- Not for commercial storefronts — GitHub's terms disallow primarily-commercial/e-commerce use; personal sites, docs, portfolios, and project pages are exactly the intended use.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| 404 at the site URL | no index.html at the published folder's root, or wrong case | rename to exactly index.html; confirm branch/folder in Settings → Pages |
| Site loads, CSS/images missing | absolute /paths on a project site; or case mismatch (Hero.PNG vs hero.png — Pages is case-sensitive, macOS isn't) | switch to relative paths; match filename case exactly |
| Changes pushed but site unchanged | deploy still running, or browser cache | check Actions tab for green ✓; hard-refresh ⌘⇧R |
Push rejected: fetch first | remote has commits you don't (e.g. README made on the site) | git pull --rebase, then push |
Files with _underscore names vanish | Jekyll processing | add .nojekyll (previous section) |
| Custom domain check stuck / red | DNS not propagated or records wrong | verify with dig example.com +short → should list the four 185.199.* IPs; wait, then re-save the domain |
| "Enforce HTTPS" greyed out | certificate still provisioning | normal for up to ~24 h after DNS goes green; check back |
| Red X on the deploy run | build error (usually Jekyll choking on a file) | click the run in Actions for the log; .nojekyll often cures it |
Launch checklist
Tick as you go — progress is saved only in this browser session.
From here, publishing is a reflex: git aa && git cm "…" && git pu — and thirty seconds later, the world sees it.