Overview
GitHub Pages hosts static sites for free with HTTPS and a custom domain, building through GitHub Actions. It fits docs, blogs, and Quartz vaults. Move to vercel for SSR, ISR, edge functions, or per-PR previews.
Stay inside the published limits
From the Pages limits (checked October 2026):
- Published site: 1 GB maximum. Source repository: 1 GB recommended.
- Bandwidth: 100 GB per month soft limit. Builds: 10 per hour soft limit.
- A deployment times out after 10 minutes.
- Pages is not for running an online business, e-commerce, or SaaS.
Put cloudflare in front of sites with sustained traffic to absorb requests and protect the bandwidth quota. End-to-end walkthrough: deploy-quartz-site.
Deploy from Actions, not from a branch
Set Settings > Pages > Source to GitHub Actions. “Deploy from a branch” runs a Jekyll build you cannot customize. The deploy job needs these permissions; without id-token: write, deploy-pages fails with a 403 because it cannot mint the OIDC token.
permissions:
contents: read
pages: write
id-token: writeSplit the workflow into build and deploy jobs
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@v7
- uses: actions/setup-node@v7
with: { node-version: 22 }
- run: npm ci
- run: npx quartz build
- uses: actions/upload-pages-artifact@v5
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@v5Action majors checked October 2026: checkout v7, setup-node v7, upload-pages-artifact v5, deploy-pages v5. cancel-in-progress: false keeps a fast follow-up push from aborting a deploy mid-upload.
Set the custom domain in settings and point DNS correctly
When publishing from Actions, GitHub creates no CNAME file; the custom domain lives in Settings > Pages. Verify the domain in your account first to prevent takeover of dangling records. Then:
- Apex: four
Arecords (185.199.108.153,185.199.109.153,185.199.110.153,185.199.111.153), matchingAAAArecords, or anALIAS/ANAMEto<user>.github.io. - Subdomain: a
CNAMEto<user>.github.io. - Check with
digthat results match those IPs.
Enforce HTTPS can take up to 24 hours to become available after DNS resolves; GitHub issues the certificate through Let’s Encrypt. Behind Cloudflare, use SSL/TLS Full (strict) and see namecheap-dns for registrar records.
Debug 404s in this order
- Base path: a project site at
user.github.io/reponeeds the framework base set to/repo/; apex and custom domains do not. - Missing
index.htmlat the artifact root: runls public/after the build. - Custom domain in Settings does not match the domain you are visiting.
Add 404.html at the artifact root for a custom not-found page.