Deploy
Halftone builds to a static site in dist/. There is no server runtime, no database and no
environment variable you have to set at runtime.
Before you deploy
Set site.url in src/config.ts to your production URL. Canonical links, the feed, the sitemap,
robots.txt and every share card URL are printed absolute from it.
Two more things the demo ships with that are not yours:
newsletter.idis the demo’s Buttondown handle. Replace it with your own, or setnewsletter.provider: nullso the subscribe block does not print.src/pages/privacy.astroandsrc/pages/terms.astroeach carry a hardcodedupdateddate. Read both pages against your own config, delete what does not apply, and set the date to the day you publish them.
Requirements
- Node.js 22.12 or newer (
.nvmrcsays22). - pnpm.
package.jsonpins the exact version inpackageManager; pnpm 10+ and Corepack switch to it on their own. To install that version yourself:
npm i -g "$(node -p "require('./package.json').packageManager.split('+')[0]")"
engines sets a floor, not a ceiling, so a host picks the newest major it offers: Vercel prints a
warning and builds on Node 24. That is fine for this theme. Pin the major in your host’s settings
if you would rather not move with them.
Builds need sharp, which ships a prebuilt binary for every platform the four hosts below run. No
system package is required.
Build
pnpm install
pnpm build
Output goes to dist/. Three things happen inside that one command, with no separate step: Astro
builds the pages and re-encodes every photograph, the share cards are drawn to dist/og/ (when
features.ogImages is on), and the Pagefind index is written to dist/pagefind/ (when
features.search is on).
pnpm preview serves dist/ locally. Use it rather than pnpm dev when you want to check search
or a non-default language: the index is a build artifact, and the localized fronts exist only after
a build.
SITE_URL overrides site.url at build time, which is what a staging deploy wants:
SITE_URL=https://staging.example.com pnpm build.
If a build prints fetch failed while fetching webfonts, the failure is cached as a zero-byte
file. Clear it and rebuild:
rm -rf .astro node_modules/.astro dist && pnpm build
find dist -name '*.woff2' -size -1k # should print nothing
Your own repository
Hosts build from a Git repository you own. The Pro repository is private and cannot be forked, and the zip carries no Git history, so push your own copy first.
If you cloned from GitHub, keep the theme repository as upstream so you can pull later releases:
git remote rename origin upstream
git remote add origin https://github.com/<you>/<your-magazine>.git
git push -u origin main
If you started from the zip:
git init -b main
git add -A
git commit -m "Start from Halftone Pro"
git remote add origin https://github.com/<you>/<your-magazine>.git
git push -u origin main
Then connect that repository to a host. The defaults are right; you only confirm the build settings.
One-click deploy buttons (Deploy to Cloudflare, Deploy to Netlify, Deploy with Vercel) clone a public repository, so Pro has none. Import your own repository from the host’s dashboard instead; private repositories are supported by all four hosts below.
Whichever host you use, SITE_URL can be set as a build environment variable there. That is the
easiest way to give a preview deploy its own address while site.url stays at your production URL.
Cloudflare Workers / Pages
Build command pnpm build, output directory dist.
Vercel
Framework preset Astro, build command pnpm build, output directory dist.
A new project is private: the build succeeds, and the deployment URL answers with a login screen (Deployment Protection, a 302 to Vercel’s SSO). Attach your domain, or turn protection off under Settings → Deployment Protection, before you send the link to anyone.
Netlify
Build command pnpm build, publish directory dist.
Two things a first deploy does on its own. pnpm-workspace.yaml makes Netlify read the project as
a monorepo and propose pnpm --filter <name>... run build; leave it, it builds correctly. And a
new site is private by default, so the .netlify.app URL answers 401 until you attach a domain or
make the site public.
GitHub Pages
Works, with one thing to watch: a project site is served from /<repo>/, so set base: '/<repo>/'
in astro.config.mjs as well as site.url. A custom domain or a <user>.github.io repository
needs neither.
CI
.github/workflows/ci.yml runs on every push to main and on every pull request: install,
pnpm check, pnpm lint, pnpm check:contrast, pnpm build, a Lighthouse run against the built
site, and an internal link check. The Lighthouse pages and thresholds are in lighthouserc.cjs,
which filters the list down to the pages that actually exist — delete a demo page and the gate
keeps working.
You do not need CI to deploy, but it is the list of things worth verifying before you ship, and it is what the theme itself is held to.