Skip to content

Deploy on git push

Markdown

After Go live, deploys should happen without you: a push to main builds and publishes, and an approval in /admin/ does the same. Two routes exist, and you should pick exactly one — running both is how a build from an old commit overwrites a newer deploy.

Workers Builds GitHub Actions
Where it runs Cloudflare’s builder, connected to your repository your repository’s Actions
Tokens you manage none — Cloudflare mints its own three repository secrets
What an approval triggers a Deploy Hook (providers.rebuild: 'deploy-hook') a repository_dispatch event (providers.rebuild: 'github-dispatch')
Choose it when you want the least setup you already deploy everything from Actions, or you want tests to run before every deploy
  1. Connect the repository.

    Cloudflare dashboard → Workers & Pages → your Worker → Settings → Build → Connect, and fill in:

    Field Value
    Repository / branch your repository, main
    Root directory the directory holding package.json and wrangler.jsonc/ for a flat copy
    Build command pnpm build
    Deploy command pnpm exec wrangler deploy — the pinned wrangler, not whatever npx fetches today
    API token leave empty; Cloudflare generates one
  2. Add the build variables.

    Under Variables and secrets on the same page. For a site that still builds from Markdown, PNPM_VERSION=10.12.4 is enough. Once the site reads the database, add LISTINGS_SOURCE=d1, CLOUDFLARE_ACCOUNT_ID, D1_DATABASE_ID, and D1_READ_TOKEN as a secret — see Environment and secrets.

  3. Push to main.

    Cloudflare builds and deploys. Other branches get a preview version and never touch production.

Versions are pinned by files already in the repository: .nvmrc, packageManager in package.json, and wrangler in devDependencies. A build takes about a minute.

Free-plan quota: 3,000 build minutes a month, 20 minutes per build, and one concurrent build per account. The concurrency limit is account-wide, so a second connected project makes builds queue — including the build an approval triggers.

Where You should see
The build ran Workers Builds → Deployments, or the Actions run a green build, about a minute long
It built from the right source the build log [zerodirs-files] listings: N from ./src/content/listings or [zerodirs-d1] D1: N approved listings — whichever you intended
It is live https://<your domain>/ the change you pushed
  1. Add the domain to your Cloudflare account as a zone.

  2. Workers & Pages → your Worker → Settings → Domains & Routes → Add → Custom domain. Cloudflare issues the certificate and creates the DNS record. pnpm cf:setup does this step for you when site.url is on a zone the account owns.

  3. Set site.url to that domain and deploy again. Canonicals, the sitemap and the feeds are built from it, so until the next build they still point at the old host.

The <name>.workers.dev URL keeps working. Turn it off in the same panel if you would rather it did not — and do turn it off if you put the admin routes behind Cloudflare Access, because *.workers.dev bypasses a zone-level Access policy. See Security.

trailingSlash: 'always' is enforced before route matching, so a request to a URL without one earns a 301 (GET) or 308 (everything else). That is invisible for a browser and fatal for a webhook: Stripe records the redirect as a failed delivery. Register the endpoint with the slash.

https://example.com/api/stripe/webhook/ ← correct
https://example.com/api/stripe/webhook ← 308, recorded as a delivery failure

The same applies to the Deploy Hook URL, the checkout redirect, the status-page links and every admin route. Nothing in the starter constructs a URL by hand: src/lib/paths.ts is the only place one is built, and a unit test asserts every dynamic path it produces ends in /.

Every deploy is a version. Workers & Pages → your Worker → Deployments → pick the previous one → Rollback. Nothing local is needed.

This is also why the build is strict rather than forgiving. An unknown category, an orphan page under seo.strictLinks, or a file count over 90% of the plan limit all stop the build — and the version already serving keeps serving. A build that fails is a deploy that did not happen, which is the outcome you want.