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

Split 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@v5

Action 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 A records (185.199.108.153, 185.199.109.153, 185.199.110.153, 185.199.111.153), matching AAAA records, or an ALIAS/ANAME to <user>.github.io.
  • Subdomain: a CNAME to <user>.github.io.
  • Check with dig that 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

  1. Base path: a project site at user.github.io/repo needs the framework base set to /repo/; apex and custom domains do not.
  2. Missing index.html at the artifact root: run ls public/ after the build.
  3. 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.