---
title: "How to deploy a Quartz site to GitHub Pages"
slug: "deploy-quartz-site"
category: "howto"
tags: ["howto", "quartz", "github-pages", "deployment", "tutorial", "ops"]
status: "stable"
last_updated: 2026-05-14
summary: "Fork Quartz, configure baseUrl and CNAME, wire the GitHub Actions deploy workflow, point a custom domain, and verify the live site with curl."
related: ["[[tooling/quartz]]", "[[ops/github-pages]]", "[[ops/namecheap-dns]]", "[[ops/cloudflare]]", "[[howto/run-a-quartz-static-site-locally]]", "[[howto/ship-llms-txt]]"]
---

## Overview

Quartz 4 plus GitHub Pages plus Cloudflare is the cheapest reliable way to host a public knowledge base. This guide ships a Quartz vault to `https://yourdomain.com` end to end: fork, configure, wire the Actions workflow, point DNS, and prove the site responds. Total time is under an hour on a fresh GitHub account.

## Prerequisites

- Node 22 installed locally. Check with `node -v`.
- A GitHub account with the ability to enable Pages on a public repo.
- A domain you control, with access to its DNS records. See [[ops/namecheap-dns]] if the domain is on Namecheap.
- Local Quartz running. See [[howto/run-a-quartz-static-site-locally]] for the local-only path.

## Steps

### 1. Fork or clone Quartz

```bash
git clone https://github.com/jackyzha0/quartz.git mysite
cd mysite
npm ci
```

Push the repo to your own GitHub account. Keep the default branch as `main`; the Actions workflow assumes it.

### 2. Set the `baseUrl` and write content

Edit `quartz.config.ts`. Set `baseUrl` to the bare domain, no protocol, no path. See [[tooling/quartz-config]] for the full config reference.

```ts
configuration: {
  pageTitle: "yoursite",
  baseUrl: "yourdomain.com",
  ignorePatterns: ["private", "templates", ".obsidian"],
  // ...
}
```

Drop your markdown into `content/`. Every page needs YAML frontmatter; see [[tooling/quartz]] for the plugin set that pays rent.

### 3. Enable `Plugin.CNAME()` and remove any manual CNAME file

In `quartz.config.ts`, confirm `Plugin.CNAME()` is in the `emitters` list. Do not place a `CNAME` file under `quartz/static/`; Quartz writes the right one at build time from `baseUrl`.

```ts
emitters: [
  Plugin.ContentPage(),
  // ...
  Plugin.CNAME(),
],
```

Run `npx quartz build` and confirm `public/CNAME` prints your domain.

### 4. Add the deploy workflow

Create `.github/workflows/deploy.yml`.

```yaml
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
        with: { fetch-depth: 0 }
      - 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: github-pages
    steps:
      - uses: actions/deploy-pages@v4
```

`fetch-depth: 0` lets Quartz read git history for `CreatedModifiedDate`. The `id-token: write` permission is required for `actions/deploy-pages@v4`; omitting it produces a 403. See [[ops/github-pages]] for the full rationale.

### 5. Enable Pages with the Actions source

In the repo, open Settings, Pages. Set Source to "GitHub Actions". Do not pick "Deploy from a branch"; that runs an opaque Jekyll build.

Push to `main`. The Actions tab should show two jobs, both green within two minutes.

### 6. Point DNS to GitHub Pages

For the apex (`yourdomain.com`), add four A records pointing at GitHub's Pages IPs: `185.199.108.153`, `185.199.109.153`, `185.199.110.153`, `185.199.111.153`. For a subdomain, add one CNAME record to `<user>.github.io`.

If you front the site with Cloudflare, set SSL/TLS mode to Full strict. See [[ops/cloudflare]].

### 7. Set the custom domain and enforce HTTPS

In Settings, Pages: enter `yourdomain.com` in "Custom domain". GitHub verifies DNS. Wait until the green check appears, then toggle "Enforce HTTPS". Provisioning can take 15 minutes; do not flip the toggle before the DNS check passes.

## Verify it worked

Three checks confirm the site is live and correctly configured.

```bash
# 1. DNS resolves to GitHub Pages IPs
dig +short yourdomain.com

# 2. HTTPS responds and the canonical content shows
curl -sI https://yourdomain.com | head -1
# expected: HTTP/2 200

# 3. The CNAME file deployed
curl -s https://yourdomain.com/CNAME
# expected: yourdomain.com
```

If all three pass, the deploy is done.

## Common errors

- `public/CNAME` is missing or wrong. Confirm `Plugin.CNAME()` is in the emitters list and `baseUrl` is the bare domain.
- `actions/deploy-pages@v4` returns 403. The workflow is missing `id-token: write` under `permissions`.
- DNS resolves but HTTPS shows a certificate error. The Let's Encrypt provisioning has not finished. Wait up to 24 hours; do not flip "Enforce HTTPS" early.
- The custom domain keeps unsetting itself. The build is overwriting `public/CNAME` with empty content; see [[ops/github-pages]] for the `Plugin.CNAME()` pattern.
- 404 on every page when the site is at `user.github.io/repo`. Add `--base /repo/` or move to an apex domain.

## Related

- [[tooling/quartz]]
- [[ops/github-pages]]
- [[ops/namecheap-dns]]
- [[ops/cloudflare]]
- [[howto/run-a-quartz-static-site-locally]]
- [[howto/ship-llms-txt]]
