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 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.
# .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@v4fetch-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 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.
// 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 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 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.
// 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 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:
{
"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.