﻿# Deploy a Static Site to Cloudflare Pages

You are adding Cloudflare Pages static site deployment to this repository using the **Direct Upload** pattern. You will build locally (or in CI) and push built assets to Cloudflare's edge network. There is no git integration on the Cloudflare side.

Follow this guide phase by phase. Do not skip phases. Where a step says **ASK** — stop and wait for the user's answer. Where a step says **INSTRUCT** — present the instructions to the user for them to complete manually. Where a step says **DO** — execute it yourself.

## Tools Available

**Primary: wrangler CLI** — Use `npx wrangler` for all Pages operations (create project, deploy, list deployments, manage secrets). This is the only tool that directly manages Cloudflare Pages.

**Secondary: Cloudflare MCP** — If configured, useful for account-level inspection only. The MCP servers do NOT have Pages-specific tools. Use them for:
- `cloudflare-docs` (`https://docs.mcp.cloudflare.com/mcp`) — look up Cloudflare documentation
- `cloudflare-bindings` (`https://bindings.mcp.cloudflare.com/mcp`) — list accounts, workers, KV, R2, D1
- `cloudflare-builds` (`https://builds.mcp.cloudflare.com/mcp`) — inspect Workers build logs (not Pages)
- `cloudflare-observability` (`https://observability.mcp.cloudflare.com/mcp`) — logs and analytics

For everything Pages-related (project creation, deployment, secrets, config download), use `npx wrangler pages`. Run `npx wrangler pages --help` to discover available subcommands. Many `list` commands accept `--json` for machine-readable output — always prefer `--json` over parsing tables.

---

## Phase 0: Understand the Repository

**DO:** First, verify that `npx` and `wrangler` are available:

```bash
npx wrangler --version
```

If this fails, Node.js is not installed. **INSTRUCT** the user to install Node.js (v18+) before continuing.

**DO:** Examine the repo and determine the following. Present your findings to the user before proceeding.

1. **Build output directory** — Where do the final static assets (HTML/CSS/JS/images) live?
   - No build step (plain HTML/assets): The repo root or a subfolder IS the output
   - Static site generator (Hugo, Jekyll, Eleventy, Astro, Quartz, etc.): Check config for output dir (`public/`, `_site/`, `dist/`, `build/`)
   - Frontend framework (Vite, Next.js static export, etc.): Usually `dist/`, `out/`, or `build/`
   - Obsidian vault: Needs a publish tool (e.g., Quartz) to produce HTML first — the vault itself is not deployable

2. **Build command** — Check `package.json` scripts, `Makefile`, `justfile`, or framework config. Record the exact command, or note "none" for pre-built/plain sites.

3. **Existing deployment config** — Check for `wrangler.toml`, `wrangler.json`, `.github/workflows/*cloudflare*`, or any deploy scripts already present.

**ASK** the user to confirm your findings, then ask:
- **Project name**: Suggest one derived from the repo name. Must be lowercase, alphanumeric, hyphens only. This becomes the `<PROJECT_NAME>.pages.dev` subdomain.
- **Custom domain**: Will the site use a custom domain? If yes, what is it? Will `www` also be needed?
- **CI/CD**: Do they want GitHub Actions automated deployment, or just local deploys for now?

Wait for answers before proceeding. Record the values:
- `PROJECT_NAME` = (user's answer)
- `BUILD_OUTPUT_DIR` = (from your analysis)
- `BUILD_COMMAND` = (from your analysis, or "none")
- `CUSTOM_DOMAIN` = (user's answer, or "none")
- `WANTS_CICD` = (yes/no)

---

## Phase 1: Verify Cloudflare Auth

**DO:** Run `npx wrangler whoami` and check the exit code.

If exit code is 0: Record the **Account ID** from the table in the output. Continue to Phase 2.

If it fails (non-zero exit code — not authenticated):

**INSTRUCT** the user:
> You need to authenticate with Cloudflare. I'll run `npx wrangler login` which will open a browser window. Please complete the OAuth flow in your browser and then let me know when it's done.
>
> If the browser doesn't open automatically, look for a URL in the terminal output and open it manually.

**DO:** Run `npx wrangler login`, then re-run `npx wrangler whoami` to confirm. Record the Account ID.

Note: OAuth login grants access based on the user's Cloudflare account permissions. No scope selection is needed here — that only matters for API tokens (covered in Phase 6).

---

## Phase 2: Create the Pages Project

**DO:** Run `npx wrangler pages project list --json` and check if `PROJECT_NAME` already exists in the output.

If it already exists: Confirm with the user that this is the right project, then skip to Phase 3.

If it does NOT exist:

**DO:** Run:
```bash
npx wrangler pages project create PROJECT_NAME --production-branch main
```

If this errors with a message about the name already being taken (by another Cloudflare account), **ASK** the user for an alternative project name. The name must be globally unique across all Cloudflare Pages projects.

This creates a Direct Upload project (no git provider). Deployments to branch `main` are production; any other branch name produces preview deployments.

---

## Phase 3: Deploy Script

Choose the approach that fits the repo. Do NOT create a `package.json` if the project isn't Node-based.

**Important: deploying the repo root.** If `BUILD_OUTPUT_DIR` is `.` (the repo root), wrangler will upload all files in the directory. It automatically excludes `.git/`, `node_modules/`, and files matched by `.gitignore`. However, be aware that any other non-web files (e.g., `README.md`, `Makefile`, config files) will be uploaded too. This is usually harmless but worth knowing.

### If the repo has a `package.json`:

**DO:** Add deploy scripts to the existing `package.json`:

If there is NO build step:
```json
{
  "scripts": {
    "deploy": "npx wrangler pages deploy BUILD_OUTPUT_DIR --project-name PROJECT_NAME --branch main --commit-hash \"$(git rev-parse HEAD)\" --commit-message \"$(git log -1 --pretty=%s)\"",
    "deploy:preview": "npx wrangler pages deploy BUILD_OUTPUT_DIR --project-name PROJECT_NAME --branch preview"
  }
}
```

If there IS a build step:
```json
{
  "scripts": {
    "build": "BUILD_COMMAND",
    "deploy": "npm run build && npx wrangler pages deploy BUILD_OUTPUT_DIR --project-name PROJECT_NAME --branch main --commit-hash \"$(git rev-parse HEAD)\" --commit-message \"$(git log -1 --pretty=%s)\"",
    "deploy:preview": "npm run build && npx wrangler pages deploy BUILD_OUTPUT_DIR --project-name PROJECT_NAME --branch preview"
  }
}
```

### If the repo does NOT have a `package.json`:

**DO:** Create a `Makefile` (or `justfile` if the project already uses `just`):

```makefile
.PHONY: deploy deploy-preview

deploy:
	BUILD_COMMAND_HERE_OR_REMOVE_THIS_LINE
	npx wrangler pages deploy BUILD_OUTPUT_DIR --project-name PROJECT_NAME --branch main \
		--commit-hash "$$(git rev-parse HEAD)" \
		--commit-message "$$(git log -1 --pretty=%s)"

deploy-preview:
	BUILD_COMMAND_HERE_OR_REMOVE_THIS_LINE
	npx wrangler pages deploy BUILD_OUTPUT_DIR --project-name PROJECT_NAME --branch preview
```

Note: In Makefiles, `$` must be escaped as `$$` for shell variable expansion.

### Why CLI flags instead of wrangler.toml

This guide uses explicit CLI flags (`--project-name`, `--branch`, positional directory) rather than a `wrangler.toml` with `pages_build_output_dir`. Reasons:
- Simpler — no config file to maintain
- More portable — the deploy command is self-documenting
- Avoids the common "missing pages_build_output_dir" warning when wrangler.toml exists but is incomplete
- A `wrangler.toml` is only needed if you add Pages Functions, KV bindings, or environment-specific variables later

If the project later needs bindings or environment overrides, a `wrangler.toml` can be added at that point:
```toml
name = "PROJECT_NAME"
pages_build_output_dir = "BUILD_OUTPUT_DIR"
compatibility_date = "YYYY-MM-DD"

[env.production]
# production-specific bindings here

[env.preview]
# preview-specific bindings here
```

---

## Phase 4: First Deploy

**DO:** Run the build (if any), then deploy:

```bash
npx wrangler pages deploy BUILD_OUTPUT_DIR --project-name PROJECT_NAME --branch main
```

**Critical: the `--branch main` flag.** Without it, wrangler infers the branch from git. If the repo has no git history, is on a detached HEAD, or is on a non-main branch, the deploy will be marked as **Preview** instead of **Production**. Always pass `--branch main` explicitly for production deploys.

**DO:** Verify the deploy succeeded programmatically:

```bash
npx wrangler pages deployment list --project-name PROJECT_NAME --environment production --json
```

Check the first entry in the JSON output:
- `Environment` should be `"Production"`
- `Branch` should be `"main"`
- `Deployment` contains the live URL (e.g., `https://<HASH>.PROJECT_NAME.pages.dev`)

**DO:** Verify the site is reachable:

```bash
curl -sI https://PROJECT_NAME.pages.dev | head -10
```

A `200` status confirms the site is live. Report the production URL to the user.

---

## Phase 5: Custom Domain (If CUSTOM_DOMAIN is not "none")

Custom domains are configured in the Cloudflare dashboard. You cannot do this via wrangler CLI. Present the following instructions to the user.

### For an apex domain (e.g., `example.com`):

**INSTRUCT** the user:

> **Custom domain setup for `CUSTOM_DOMAIN`:**
>
> Your domain's nameservers must point to Cloudflare. If they don't already:
> 1. Log into the [Cloudflare dashboard](https://dash.cloudflare.com)
> 2. Add `CUSTOM_DOMAIN` as a zone (if not already added)
> 3. Cloudflare will assign you two nameservers (e.g., `ns1.cloudflare.com`, `ns2.cloudflare.com`)
> 4. At your domain registrar, update the nameservers to the ones Cloudflare assigned
> 5. Wait for propagation (usually minutes, can take up to 24h)
>
> Once the zone is active:
> 1. Go to **Workers & Pages** in the Cloudflare dashboard
> 2. Select the **PROJECT_NAME** project
> 3. Go to the **Custom domains** tab
> 4. Click **Set up a domain**
> 5. Enter `CUSTOM_DOMAIN` and confirm
> 6. Cloudflare will automatically create the required DNS record
>
> Let me know when this is done so I can verify.

### For `www` subdomain (if requested):

**INSTRUCT** the user:

> After the apex domain is active, repeat the same process:
> 1. **Custom domains** tab > **Set up a domain**
> 2. Enter `www.CUSTOM_DOMAIN`
> 3. Cloudflare auto-creates the CNAME record

### For a subdomain on external DNS (e.g., `blog.example.com`):

**INSTRUCT** the user:

> 1. In the Cloudflare dashboard: **Workers & Pages** > **PROJECT_NAME** > **Custom domains** > **Set up a domain**
> 2. Enter the subdomain (e.g., `blog.example.com`)
> 3. Then at your DNS provider, add a CNAME record:
>
> | Type  | Name               | Content                    |
> |-------|--------------------|----------------------------|
> | CNAME | `blog.example.com` | `PROJECT_NAME.pages.dev`   |
>
> **Important:** You must register the domain in the Pages dashboard FIRST, then add the DNS record. Doing it backwards causes a 522 error.

After the user confirms the domain setup:

**DO:** Verify by checking the project's domain list:

```bash
npx wrangler pages project list --json
```

The `Project Domains` field for `PROJECT_NAME` should now include the custom domain(s). Then confirm it resolves:

```bash
curl -sI https://CUSTOM_DOMAIN | head -10
```

If `curl` returns a connection error, DNS may still be propagating. **INSTRUCT** the user: "DNS propagation can take a few minutes. If the domain doesn't resolve yet, wait 5-10 minutes and try again. It can take up to 24 hours in rare cases."

---

## Phase 6: CI/CD with GitHub Actions (If WANTS_CICD is "yes")

**ASK** the user:
- Do they already have GitHub secrets set up for Cloudflare? (`CLOUDFLARE_API_TOKEN`, `CLOUDFLARE_ACCOUNT_ID`)
- If not, they will need to create an API token.

**INSTRUCT** the user (if they need to create secrets):

> You need two GitHub repository secrets:
>
> 1. **`CLOUDFLARE_ACCOUNT_ID`**: Your account ID is `<the Account ID recorded in Phase 1>`
>
> 2. **`CLOUDFLARE_API_TOKEN`**: Create one at https://dash.cloudflare.com/profile/api-tokens
>    - Use the "Custom token" template
>    - Permissions: **Account** > **Cloudflare Pages** > **Edit**
>    - Account resources: Include the relevant account
>
> Add both as secrets at: `https://github.com/<OWNER>/<REPO>/settings/secrets/actions`
>
> Let me know when done.

After confirmation:

**DO:** Create `.github/workflows/deploy.yml`:

```yaml
name: Deploy to Cloudflare Pages

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
  workflow_dispatch:

jobs:
  deploy:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      deployments: write
    steps:
      - uses: actions/checkout@v4

      # BUILD_STEP_HERE (remove this comment block if no build step)
      # Example for Node projects:
      # - uses: actions/setup-node@v4
      #   with:
      #     node-version: "20"
      # - run: npm ci && npm run build

      - name: Deploy to Cloudflare Pages
        uses: cloudflare/wrangler-action@v3
        with:
          apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
          command: pages deploy BUILD_OUTPUT_DIR --project-name=PROJECT_NAME
```

Uncomment and adapt the build step if needed. The wrangler action infers the branch from git context — this is reliable in CI because GitHub Actions checks out the correct branch. Pushes to `main` deploy to production; PR branches deploy as previews with unique URLs.

---

## Phase 7: Gitignore

**DO:** Ensure `.gitignore` includes:

```gitignore
# Wrangler local state
.wrangler/
```

If the build output directory is generated (e.g., `dist/`, `public/` from a build step), also add it to `.gitignore`. If there is no build step and the repo root IS the content, no additional entries are needed.

---

## Phase 8: Verify and Report

**DO:** Run through this checklist programmatically and report results to the user.

1. **Project exists:**
   ```bash
   npx wrangler pages project list --json
   ```
   Confirm `PROJECT_NAME` appears in output.

2. **Latest deploy is production:**
   ```bash
   npx wrangler pages deployment list --project-name PROJECT_NAME --environment production --json
   ```
   Confirm the latest entry shows `Environment: "Production"` and `Branch: "main"`.

3. **Site is reachable:**
   ```bash
   curl -sI https://PROJECT_NAME.pages.dev | head -5
   ```
   Confirm HTTP 200.

4. **Custom domain resolves** (if configured):
   ```bash
   curl -sI https://CUSTOM_DOMAIN | head -5
   ```
   Confirm HTTP 200. If it fails, advise waiting for DNS propagation.

5. **GitHub Actions workflow exists** (if requested):
   Confirm `.github/workflows/deploy.yml` is present and properly configured.

Present a summary to the user:

> **Deployment configured.** Here's what was set up:
> - Project: `PROJECT_NAME` on Cloudflare Pages (Direct Upload)
> - Production URL: `https://PROJECT_NAME.pages.dev`
> - Custom domain: `CUSTOM_DOMAIN` (if applicable)
> - Deploy command: `<the actual command from Phase 3>`
> - CI/CD: GitHub Actions on push to main (if applicable)


---

## Reference: Discovering wrangler Commands

Do not memorize command signatures — they change across wrangler versions. Instead, use `--help` to discover available commands and their flags at each level:

```bash
npx wrangler --help              # Top-level commands
npx wrangler pages --help        # Pages subcommands (deploy, project, deployment, secret, download)
npx wrangler pages deploy --help # Deploy flags (--project-name, --branch, --commit-hash, etc.)
npx wrangler pages project --help       # Project management (list, create, delete)
npx wrangler pages deployment --help    # Deployment inspection (list with --json, --environment)
npx wrangler pages secret --help        # Secret management (put, list, delete, bulk)
```

Key patterns to know:
- Most `list` commands accept `--json` for machine-readable output — always prefer this over parsing tables
- `--project-name` (or `--project`) is required on most commands when there is no `wrangler.toml`
- `--help` at any level shows all available flags and positional arguments

## Reference: Cloudflare MCP Setup

If the AI client does not yet have Cloudflare MCP configured, here is the config to add:

```json
{
  "mcpServers": {
    "cloudflare-docs": {
      "command": "npx",
      "args": ["mcp-remote", "https://docs.mcp.cloudflare.com/mcp"]
    },
    "cloudflare-bindings": {
      "command": "npx",
      "args": ["mcp-remote", "https://bindings.mcp.cloudflare.com/mcp"]
    },
    "cloudflare-builds": {
      "command": "npx",
      "args": ["mcp-remote", "https://builds.mcp.cloudflare.com/mcp"]
    },
    "cloudflare-observability": {
      "command": "npx",
      "args": ["mcp-remote", "https://observability.mcp.cloudflare.com/mcp"]
    }
  }
}
```

Each server uses OAuth — a browser window opens on first use for the user to authenticate.

These MCP servers are useful for account-level inspection but do NOT have Pages-specific tools. All Pages operations go through `npx wrangler pages`.

## Troubleshooting

| Problem | Cause | Fix |
|---------|-------|-----|
| `npx wrangler` not found | Node.js not installed | Install Node.js v18+ |
| Deploy says "Preview" not "Production" | Branch was not `main` (detached HEAD, wrong branch, or no git repo) | Pass `--branch main` explicitly |
| Project create fails "name already taken" | Another Cloudflare account owns that project name | Choose a different project name — names are globally unique |
| wrangler.toml warning "missing pages_build_output_dir" | wrangler.toml exists but has no `pages_build_output_dir` | Either add the field or delete wrangler.toml and use CLI flags |
| Custom domain shows 522 error | DNS record added before registering in Pages dashboard | Register domain in Pages dashboard first, then add DNS |
| Custom domain doesn't resolve after setup | DNS propagation delay | Wait 5-10 minutes, can take up to 24h in rare cases |
| `npx wrangler pages deploy` prompts for project name | No wrangler.toml and no `--project-name` flag | Always pass `--project-name` when not using wrangler.toml |
| Large file rejected | Cloudflare Pages has 25 MiB per-file limit | Optimize assets, or use R2 for large files and link to them |
| `wrangler login` hangs | Browser didn't open or OAuth wasn't completed | Ask user to check browser for the Cloudflare auth page, or look for a URL in terminal output |
| Deploy succeeds but site shows old content | CDN cache or deployed to preview instead of production | Run `deployment list --json` to verify environment, wait a moment for cache |
| Unwanted files uploaded when deploying `.` | Repo root includes non-web files | Wrangler auto-excludes `.git/`, `node_modules/`, and `.gitignore` matches; other files upload but are harmless |
