---
title: "Quartz Deployment"
slug: "quartz-deployment"
category: "tooling"
tags: ["tooling", "quartz", "deployment", "github-pages", "cloudflare", "vercel", "ci"]
status: "stable"
last_updated: 2026-05-14
summary: "Deploying Quartz to GitHub Pages, Cloudflare Pages, and Vercel; wiring custom domains; setting cache headers; and the Plugin.CNAME pattern."
related: ["[[tooling/quartz]]", "[[tooling/quartz-config]]", "[[tooling/quartz-plugins]]", "[[ops/github-pages]]", "[[ops/cloudflare]]", "[[ops/vercel]]", "[[howto/deploy-quartz-site]]"]
---

## Overview

Quartz builds to a `public/` directory of static files. Any static host accepts it. GitHub Pages is the default for vault-style projects: free, Actions-native, and simple. Cloudflare Pages adds unlimited bandwidth and per-PR previews. Vercel fits when the site is part of a Next.js monorepo. All three require the same upfront step: setting `baseUrl` correctly in [[tooling/quartz-config]] before the first deploy.

## Deploy to GitHub Pages with the two-job workflow

The build job produces the artifact; the deploy job publishes it. Split them to keep the deploy environment isolated.

```yaml
# .github/workflows/deploy.yml
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:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    steps:
      - id: deployment
        uses: actions/deploy-pages@v4
```

`fetch-depth: 0` is required so Quartz can read full git history for `CreatedModifiedDate`. `id-token: write` lets `deploy-pages@v4` mint the OIDC token that Pages validates; omitting it produces a 403. `cancel-in-progress: false` prevents a second push from aborting a mid-flight deploy. For the full rationale, see [[ops/github-pages]].

## Use `Plugin.CNAME()` for custom-domain GitHub Pages deploys

The `CNAME` file at the build root tells GitHub Pages which custom domain to serve the site on. Quartz provides an emitter that writes it automatically.

```ts
// quartz.config.ts
emitters: [
  Plugin.ContentPage(),
  // ...
  Plugin.CNAME(),
],
configuration: {
  baseUrl: "llmbestpractices.com",  // the CNAME file content comes from here
}
```

Never place a `CNAME` file in `quartz/static/`. That path copies to `public/static/CNAME`, which Pages ignores. The plugin writes `public/CNAME` at the correct path. After every local build, run `cat public/CNAME` to confirm the file exists and contains the bare domain. See [[tooling/quartz-config]] for `baseUrl` formatting rules.

## Deploy to Cloudflare Pages by pointing at the Git repo

Connect the GitHub repo to a Cloudflare Pages project in the dashboard. Set the build command and output directory.

```
Build command:   npx quartz build
Output directory: public
Node version:    22
```

Set the `NODE_VERSION` environment variable to `22` under Settings, Environment variables, Production. Cloudflare Pages uses Node 18 by default; Quartz requires Node 22.

Per-branch preview URLs are automatic. Cloudflare builds every push to every branch and assigns it a preview URL. Use this to review draft content before merging. For the DNS and SSL wiring, see [[ops/cloudflare]].

## Deploy to Vercel when the Quartz site is inside a monorepo

Vercel works well when `content/` lives inside a larger monorepo. Point the Vercel project root at the Quartz subfolder.

```json
// vercel.json at the Quartz folder root
{
  "buildCommand": "npx quartz build",
  "outputDirectory": "public",
  "installCommand": "npm ci"
}
```

Set the Node version to 22 in the Vercel project settings. Vercel defaults to the version specified in `package.json` `engines.node`; set that field to `"22"` and Vercel respects it. For full Vercel configuration, see [[ops/vercel]].

## Set aggressive cache headers for static assets

Quartz emits versioned asset filenames for JS and CSS chunks. Configure the host to send long-lived cache headers for those paths.

For Cloudflare, add a Cache Rule: URL path matches `*.js`, `*.css`, `*.woff2` → Cache Behavior: Override, Edge TTL 1 year, Browser TTL 1 year.

For GitHub Pages, caching is managed by Cloudflare in front of the origin. Set the Cloudflare rule to cache everything and respect edge TTL. Pages itself does not support custom response headers.

For Vercel, add headers in `vercel.json`:

```json
{
  "headers": [
    {
      "source": "/(.*)\\.(:ext|js|css|woff2)",
      "headers": [{ "key": "Cache-Control", "value": "public, max-age=31536000, immutable" }]
    }
  ]
}
```

## Use `workflow_dispatch` for manual redeploys without a content change

Adding `workflow_dispatch:` to the `on` block lets you trigger a build from the Actions tab without pushing a commit. Use it to force a redeploy after changing DNS or enabling HTTPS in the Pages settings.

## Related

- [[tooling/quartz]]
- [[tooling/quartz-config]]
- [[tooling/quartz-plugins]]
- [[ops/github-pages]]
- [[ops/cloudflare]]
- [[ops/vercel]]
- [[howto/deploy-quartz-site]]
