# Deploying a site to Cloudflare Pages

How to put a static or Next.js site on Cloudflare Pages via `wrangler`, the way `realagent.pages.dev` is deployed. This covers the build → deploy → verify loop and the Next.js specifics. Auth is the reused per-scope token per `api.md`; if the site also captures form data, read `d1-data-capture.md` for the dual Pages-Edit **and** D1-Edit scope.

This is distinct from `references/serving-published-sites.md`, which serves a platform-published site at a custom domain through the brand's **cloudflared tunnel**. This reference is about **Cloudflare Pages** — Cloudflare hosts the build, not the install device.

---

## When to use Pages vs the tunnel

- **Cloudflare Pages** (this reference) — a standalone marketing/landing/app site that Cloudflare builds and serves on its edge, optionally with Pages Functions + D1. Use when the site is its own project and you want Cloudflare to host it.
- **The brand tunnel** (`manual-setup.md` + `serving-published-sites.md`) — expose something running on the install device at a custom domain. Use when the content is served by the platform itself.

---

## 1. Build the site

Produce the static output the framework emits.

- **Static / Vite / Astro:** `npm run build` → output in `dist/` (or the framework default).
- **Next.js:** Pages needs a static or edge-compatible build. For a fully static site use `output: "export"` in `next.config.js` and `npm run build` → output in `out/`. For a Next.js app with server routes, deploy through the Pages adapter (`@cloudflare/next-on-pages`): `npx @cloudflare/next-on-pages` → output in `.vercel/output/static`.

## 2. Configure `wrangler.toml`

```toml
name = "<project>"                       # the Pages project name, e.g. realagent
pages_build_output_dir = "<output-dir>"  # dist | out | .vercel/output/static
compatibility_date = "2024-01-01"
compatibility_flags = ["nodejs_compat"]  # required for next-on-pages and most Functions
```

If the site captures form data, add the `[[d1_databases]]` binding from `d1-data-capture.md` here.

## 3. Load the reused deploy token

> **On a multi-tenant install, a client account does not run this step at all.** After the 1631 storage isolation, a client's `cloudflare.env` carries no master, and `cf-token.sh` denies it the `pages` scope (`reason=not-house`) — correctly, since that master is account-wide. A client account publishes with the `storage-pages-deploy` tool, which runs the deploy house-side; the project must already be recorded to the account by the house (`storage-pages-adopt`), because the project-to-folder mapping is not derivable and is never guessed. The rest of this section applies to the **house account and single-tenant installs**, where the master still lives in the account secrets file. If you are a client account and you reached a `not-house` denial here, that is this branch, not a broken token: use the tool. The tool reads `pages_build_output_dir` from the site root's `wrangler.toml` (or `wrangler.json`/`.jsonc`) and publishes that directory as the served root, which is the same asset root step 4 below passes positionally. A declared output dir that is missing fails the deploy naming the path rather than publishing its parent.

A Pages deploy needs **Account · Cloudflare Pages · Edit**. If the site has Pages Functions hitting D1, the token also needs **Account · D1 · Edit** (the single most common breakage — see `d1-data-capture.md`). The `pages` scope resolves the stable `<brand>-pages-d1` token, which carries both. Resolve it with the deterministic helper, which mints it once only if absent and reuses it thereafter. This is the one command; do not source the secrets file and hand the resulting `CLOUDFLARE_API_TOKEN` to `wrangler` — that variable holds the **master**, not the per-scope token, and `wrangler pages` then returns `Authentication error [code: 10000]`:

```bash
KEY=$(bash platform/plugins/cloudflare/bin/cf-token.sh pages "${SECRETS_DIR}/cloudflare.env")
set -a; . "${SECRETS_DIR}/cloudflare.env"; set +a
: "${CLOUDFLARE_ACCOUNT_ID:?account id not loaded}"
PAGES_D1="${!KEY}"   # the active Pages(+D1) per-scope token — NOT the sourced CLOUDFLARE_API_TOKEN (that is the master)
: "${PAGES_D1:?per-scope token not resolved — re-run cf-token.sh and read its output}"
```

A `10000` here is a token-selection symptom, not a permissions wall: it means the master reached `wrangler`. Re-run the helper line and retry with `${PAGES_D1}`; never fall back to a manual step on the strength of a `10000`. The mint-once, reuse, prefix-routing, and redaction details live in `api.md` § Provisioning and reusing a stable per-scope token; this command is self-contained and does not depend on reading that section first.

## 4. Deploy

```bash
CLOUDFLARE_API_TOKEN="${PAGES_D1}" wrangler pages deploy <output-dir> \
  --project-name <project> --branch=main
```

`--branch=main` deploys to the production alias (`https://<project>.pages.dev`); any other branch name produces a preview URL instead. The first deploy of a new project name creates the project. The token value is never printed — `wrangler` prints the deployment URL, which is what gets surfaced.

## 5. Custom domain (optional)

Attach a custom domain to the Pages project in the dashboard (**Workers & Pages → your project → Custom domains → Set up a domain**), or via the API. Cloudflare provisions the certificate and the CNAME automatically when the zone is on the same account.

---

## Outcome contract

Hosting is done when the deployed URL serves the **new** build:

```bash
curl -s "https://<project>.pages.dev/" | head
```

The response showing the new markup is the proof. A successful `wrangler` exit alone is not the contract — the live URL returning the new content is. Surface the operation as verb + target ("deploying Pages project `realagent`"); the token value never appears in chat or stdout.
