Ship It

Put your site on the internet, on your own domain, that redeploys itself every time you push — and only if the tests pass.

You built the thing. Now put it where people can reach it. This guide takes a static site from your laptop to a real URL on your own domain, wired so every git push rebuilds and redeploys it automatically — and a broken build or dead link blocks the deploy so production only ever moves forward.

We use Cloudflare Pages for hosting (free, fast, global) and GitHub Actions for the pipeline. This is the exact setup behind the site you’re reading. By the end, shipping is one command: git push.

What you’ll have at the end

  • Your site live at a real domain over HTTPS.
  • A push-to-deploy pipeline: git push → build → deploy, no dashboards to click.
  • A test gate: the deploy only runs if the build succeeds (add link/smoke checks and those gate it too).
  • Zero secrets in your repo — the two credentials live in GitHub, encrypted.

Before you start

  • A project that builds to static files (this uses Astro’s dist/; any static output works). Confirm npm run build produces a dist/ folder locally first.
  • The code in a GitHub repo (git init, push to a new repo if it isn’t already).
  • A Cloudflare account (free) → https://dash.cloudflare.com.
  • A domain you can point at Cloudflare (or skip that — you get a free *.pages.dev URL).
  • ~30–60 minutes, most of it one-time setup you never touch again.

Step 1 — Confirm the build locally

The pipeline runs the same command CI will, so get it green on your machine first:

npm install
npm run build      # must produce ./dist with your pages in it
npm run preview    # optional: eyeball the production build at localhost:4321

If dist/ has your index.html and pages, you’re ready. If the build errors, fix it here — never debug a build inside CI.


Step 2 — Get a Cloudflare API token and account ID

The pipeline needs permission to deploy on your behalf. Two values:

  1. API token — Cloudflare dashboard → My Profile → API Tokens → Create Token. Use the “Edit Cloudflare Workers” template, or a custom token with the Account → Cloudflare Pages → Edit permission. Copy the token (shown once).
  2. Account ID — on the dashboard’s right sidebar (or any zone’s Overview page). Copy it.

Treat the token like a password. It can deploy to your account. It goes into GitHub’s encrypted secrets in the next step — never into a file in the repo.


Step 3 — Store them as GitHub secrets

In your repo on GitHub: Settings → Secrets and variables → Actions → New repository secret. Add two:

  • CLOUDFLARE_API_TOKEN → the token from Step 2
  • CLOUDFLARE_ACCOUNT_ID → the account ID from Step 2

GitHub encrypts these; the workflow reads them at runtime and they never appear in logs.


Step 4 — Create the Pages project once

Do this a single time so the project exists for the pipeline to deploy into. Locally:

npx wrangler pages project create my-site --production-branch main

Replace my-site with your project name (remember it — it goes in the workflow below). It first opens a browser to authorize Wrangler; approve it. That’s the only manual deploy you’ll ever do.


Step 5 — The deploy pipeline

Create .github/workflows/deploy.yml. On every push to main it builds, then deploys — and the deploy needs the build job to pass first:

name: Test and deploy

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]   # PRs build (catch breakage) but don't deploy
  workflow_dispatch:

permissions:
  contents: read

jobs:
  build:
    name: Build
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: npm
      - run: npm ci
      - run: npm run build   # a failure here stops everything below

  deploy:
    name: Deploy to Cloudflare Pages
    needs: build                          # only deploy if build passed
    if: github.event_name == 'push'       # don't deploy on PRs
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: npm
      - run: npm ci
      - run: npm run build
      - name: Deploy
        uses: cloudflare/wrangler-action@v3
        with:
          apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
          command: pages deploy dist --project-name=my-site --branch=main

The whole point is needs: build. The deploy job won’t start until the build job is green. Add test steps to the build job (a link checker, smoke tests) and each one becomes a gate — production can’t regress past a passing suite. Change my-site to your project name in the deploy command.

Commit and push:

git add .github/workflows/deploy.yml
git commit -m "ci: deploy to Cloudflare Pages on push"
git push

Open your repo’s Actions tab and watch it run. When it’s green, your site is live at https://my-site.pages.dev.


Step 6 — Put it on your own domain

If your domain’s DNS is on Cloudflare: in the dashboard go to Workers & Pages → your project → Custom domains → Set up a custom domain, enter yourdomain.com, and Cloudflare adds the DNS record and provisions HTTPS for you (a minute or two).

If your DNS is elsewhere, either move the domain’s nameservers to Cloudflare (recommended, unlocks the one-click flow above) or add the CNAME Cloudflare shows you at your current registrar.

Within a few minutes https://yourdomain.com serves your site, certificate and all.


You now have

A live site on your own domain that ships itself. Your deploy process is now just your git workflow: push to main, tests run, and if they pass the new version goes live in under a minute. No FTP, no manual builds, no “works on my machine.”


Make it yours (next ideas)

  • Add real gates: put a link checker and smoke tests in the build job so a dead link or broken page blocks the deploy, not just a failed compile.
  • Preview deploys: Cloudflare Pages builds a unique URL for every branch/PR — great for reviewing a change before it hits main.
  • Env vars: if your app needs secrets at runtime (an API key), set them in the Pages project’s Settings → Environment variables, not in the repo.

Troubleshooting

  • Deploy step: “Project not found” → the --project-name doesn’t match, or Step 4 wasn’t run. Create the project once, then match the name exactly.
  • “Authentication error” on deploy → the CLOUDFLARE_API_TOKEN secret is missing, misspelled, or lacks the Pages Edit permission. Recreate the token and re-add it.
  • Build passes locally but fails in CI → you likely rely on an uncommitted file or a global dependency. CI starts clean: commit everything and pin deps with a lockfile (npm ci needs package-lock.json).
  • Site deploys but the domain 404s → DNS hasn’t propagated or the custom domain isn’t attached to this project yet. Give it a few minutes; confirm the record in Cloudflare.