Overview
GitHub Pages is the right host for static sites that fit inside the free tier. Bandwidth and storage are generous for personal and project sites, the build runs inside GitHub Actions, and a custom domain plus HTTPS comes free. Reach past it only when you need server rendering, edge functions, or per-PR preview URLs.
Use GitHub Pages when the site is static and small
Pick GitHub Pages for documentation, marketing pages, blogs, and Quartz vaults. The free tier caps each site at 1 GB on disk and 100 GB of bandwidth per month soft limit. Sites with sustained traffic should put cloudflare in front to absorb the requests and protect the quota.
Move to vercel when you need SSR, edge functions, ISR, or per-PR previews.
Deploy via Actions, not “deploy from branch”
Set the Pages source to GitHub Actions in the repo settings under Pages. The legacy “Deploy from a branch” mode runs an opaque Jekyll build and locks you out of build steps. Actions-mode publishes whatever artifact you upload.
The deploy job needs exactly these permissions:
permissions:
contents: read
pages: write
id-token: writeid-token: write is required for actions/deploy-pages@v4 to mint the OIDC token Pages checks. Omitting it produces a cryptic 403.
Wire the workflow with two jobs
Split build and deploy. The build job produces the artifact; the deploy job consumes it. This keeps the deploy environment isolated and matches the official template.
name: Deploy
on:
push:
branches: [main]
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: pages
cancel-in-progress: false
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 22 }
- run: npm ci
- run: npx quartz build
- uses: actions/upload-pages-artifact@v3
with: { path: public }
deploy:
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- id: deployment
uses: actions/deploy-pages@v4Set cancel-in-progress: false so a fast follow-up push does not abort a deploy mid-upload and leave the site half-published.
Put the CNAME file at the build root
Add a CNAME file (no extension, single line) containing the apex domain or subdomain. The file must end up at the root of the uploaded artifact, not inside static/ or assets/. For Quartz, put it at quartz/static/CNAME so the build copies it to public/CNAME.
GitHub Pages reads CNAME after each deploy and rewrites the custom-domain setting. If the file disappears from a build, the custom domain unsets. Pin it in source control.
Let GitHub provision HTTPS through Let’s Encrypt
Once DNS resolves to the Pages IPs, GitHub provisions a Let’s Encrypt certificate automatically. The toggle is “Enforce HTTPS” in repo settings under Pages. Wait up to 24 hours after pointing DNS before flipping it on; the issuance check fails silently if it runs early.
For DNS, see namecheap-dns and route the apex through cloudflare with SSL/TLS mode set to Full strict.
Debug 404s in this order
Most Pages 404s have one of three causes.
- Wrong base path. If the site is at
user.github.io/repo, the static build needs--base /repo/or equivalent. Apex domains do not need a base. - Missing
index.htmlat the artifact root. Runls public/after the build; the file must exist. CNAMEmismatched against the configured custom domain. The file content and the repo setting must match exactly.
For long-running content sites, add a custom 404 page at 404.html in the build output; Pages serves it for any unmatched path.