Back to skills
SkillHub ClubRun DevOpsFull StackDevOpsIntegration

cloudflare-pages

Deploy full-stack applications on Cloudflare Pages. Covers Git integration, Direct Upload, Wrangler CLI, build configuration, Pages Functions (file-based routing), bindings, headers/redirects, custom domains, environment variables. Keywords: Cloudflare Pages, Pages Functions, Git deployment, Direct Upload, Wrangler, pages.dev, _headers, _redirects, _routes.json, preview deployments.

Packaged view

This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.

Stars
10
Hot score
84
Updated
March 20, 2026
Overall rating
C1.4
Composite score
1.4
Best-practice grade
C62.8

Install command

npx @skill-hub/cli install itechmeat-llm-code-cloudflare-pages
deploymentcloudflareserverlesshostingci-cd

Repository

itechmeat/llm-code

Skill path: skills/cloudflare-pages

Deploy full-stack applications on Cloudflare Pages. Covers Git integration, Direct Upload, Wrangler CLI, build configuration, Pages Functions (file-based routing), bindings, headers/redirects, custom domains, environment variables. Keywords: Cloudflare Pages, Pages Functions, Git deployment, Direct Upload, Wrangler, pages.dev, _headers, _redirects, _routes.json, preview deployments.

Open repository

Best for

Primary workflow: Run DevOps.

Technical facets: Full Stack, DevOps, Integration.

Target audience: everyone.

License: Unknown.

Original source

Catalog source: SkillHub Club.

Repository owner: itechmeat.

This is still a mirrored public skill entry. Review the repository before installing into production workflows.

What it helps with

  • Install cloudflare-pages into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
  • Review https://github.com/itechmeat/llm-code before adding cloudflare-pages to shared team environments
  • Use cloudflare-pages for development workflows

Works across

Claude CodeCodex CLIGemini CLIOpenCode

Favorites: 0.

Sub-skills: 0.

Aggregator: No.

Original source / Raw SKILL.md

---
name: cloudflare-pages
description: "Deploy full-stack applications on Cloudflare Pages. Covers Git integration, Direct Upload, Wrangler CLI, build configuration, Pages Functions (file-based routing), bindings, headers/redirects, custom domains, environment variables. Keywords: Cloudflare Pages, Pages Functions, Git deployment, Direct Upload, Wrangler, pages.dev, _headers, _redirects, _routes.json, preview deployments."
---

# Cloudflare Pages

Full-stack application hosting with Git-based or Direct Upload deployments to Cloudflare's global network.

## Quick Navigation

- Deployment methods → `references/deployment.md`
- Build configuration → `references/build.md`
- Pages Functions → `references/functions.md`
- Bindings → `references/bindings.md`
- Headers & Redirects → `references/headers-redirects.md`
- Custom domains → `references/domains.md`
- Wrangler CLI → `references/wrangler.md`

## When to Use

- Deploying static sites or JAMstack applications
- Building full-stack apps with serverless functions
- Configuring Git-based CI/CD deployments
- Using Direct Upload for prebuilt assets
- Setting up preview deployments for branches/PRs
- Configuring custom domains and redirects

## Deployment Methods

| Method          | Best For                      | Limits                               |
| --------------- | ----------------------------- | ------------------------------------ |
| Git integration | CI/CD from GitHub/GitLab      | Cannot switch to Direct Upload later |
| Direct Upload   | Prebuilt assets, CI pipelines | Wrangler: 20k files, 25 MiB/file     |
| C3 CLI          | New project scaffolding       | Framework-dependent                  |

### Quick Deploy

```bash
# Create project
npx wrangler pages project create my-project

# Deploy
npx wrangler pages deploy ./dist

# Preview deployment (branch)
npx wrangler pages deploy ./dist --branch=feature-x
```

## Build Configuration

```bash
# Framework presets (command → output directory)
# React (Vite): npm run build → dist
# Next.js:      npx @cloudflare/next-on-pages@1 → .vercel/output/static
# Nuxt.js:      npm run build → dist
# Astro:        npm run build → dist
# SvelteKit:    npm run build → .svelte-kit/cloudflare
# Hugo:         hugo → public
```

### Environment Variables (build-time)

| Variable              | Value          |
| --------------------- | -------------- |
| `CF_PAGES`            | `1`            |
| `CF_PAGES_BRANCH`     | Branch name    |
| `CF_PAGES_COMMIT_SHA` | Commit SHA     |
| `CF_PAGES_URL`        | Deployment URL |

## Pages Functions

File-based routing in `/functions` directory:

```
/functions/index.js       → example.com/
/functions/api/users.js   → example.com/api/users
/functions/users/[id].js  → example.com/users/:id
```

```javascript
// functions/api/hello.js
export function onRequest(context) {
  return new Response("Hello from Pages Function!");
}
```

### Handler Types

| Export          | Trigger     |
| --------------- | ----------- |
| `onRequest`     | All methods |
| `onRequestGet`  | GET only    |
| `onRequestPost` | POST only   |

### Context Object

```typescript
interface EventContext {
  request: Request;
  env: Env; // Bindings
  params: Params; // Route parameters
  waitUntil(promise: Promise<any>): void;
  next(): Promise<Response>;
  data: Record<string, any>;
}
```

## Bindings

Access Cloudflare resources via `context.env`:

| Binding    | Access Pattern                           |
| ---------- | ---------------------------------------- |
| KV         | `context.env.MY_KV.get("key")`           |
| R2         | `context.env.MY_BUCKET.get("file")`      |
| D1         | `context.env.MY_DB.prepare("...").all()` |
| Workers AI | `context.env.AI.run(model, input)`       |

> For detailed binding configuration, see: `cloudflare-workers` skill.

## Headers & Redirects

Create `_headers` and `_redirects` in build output directory.

```txt
# _headers
/*
  X-Frame-Options: DENY
/static/*
  Cache-Control: public, max-age=31536000, immutable
```

```txt
# _redirects
/old-page /new-page 301
/blog/* https://blog.example.com/:splat
```

> **Warning:** `_headers` and `_redirects` do NOT apply to Pages Functions responses.

## Functions Invocation Routes

Control when Functions are invoked with `_routes.json`:

```json
{
  "version": 1,
  "include": ["/api/*"],
  "exclude": ["/static/*"]
}
```

## Wrangler Configuration

```jsonc
// wrangler.jsonc
{
  "name": "my-pages-app",
  "pages_build_output_dir": "./dist",
  "compatibility_date": "2024-01-01",
  "kv_namespaces": [{ "binding": "KV", "id": "<NAMESPACE_ID>" }],
  "d1_databases": [{ "binding": "DB", "database_name": "my-db", "database_id": "<ID>" }]
}
```

### Local Development

```bash
npx wrangler pages dev ./dist

# With bindings
npx wrangler pages dev ./dist --kv=MY_KV --d1=MY_DB=<ID>
```

## Critical Prohibitions

- Do NOT expect `_headers`/`_redirects` to apply to Pages Functions responses — attach headers in code
- Do NOT convert Direct Upload project to Git integration — not supported
- Do NOT exceed redirect limits — 2,000 static + 100 dynamic redirects max
- Do NOT use absolute URLs for proxying in `_redirects` — relative URLs only
- Do NOT edit bindings in dashboard when using Wrangler config — file is source of truth
- Do NOT store secrets in `wrangler.toml` — use dashboard or `.dev.vars` for local

## Common Gotchas

| Issue                  | Solution                                              |
| ---------------------- | ----------------------------------------------------- |
| Functions not invoked  | Check `_routes.json` include/exclude patterns         |
| Headers not applied    | Ensure not a Functions response; attach in code       |
| Build fails            | Check build command exit code (must be 0)             |
| Custom domain inactive | Verify DNS CNAME points to `<site>.pages.dev`         |
| Preview URLs indexed   | Default `X-Robots-Tag: noindex` applied automatically |

## Quick Recipes

### Conditional Build Command

```bash
#!/bin/bash
if [ "$CF_PAGES_BRANCH" == "production" ]; then
  npm run build:prod
else
  npm run build:dev
fi
```

### SPA Fallback (404.html)

Upload `404.html` in build output root for SPA routing.

### Disable Functions for Static Assets

```json
// _routes.json
{
  "version": 1,
  "include": ["/api/*"],
  "exclude": ["/*"]
}
```

## Related Skills

- `cloudflare-workers` — Worker runtime, bindings API details
- `cloudflare-d1` — D1 SQL database operations
- `cloudflare-r2` — R2 object storage
- `cloudflare-kv` — KV namespace operations


---

## Referenced Files

> The following files are referenced in this skill and included for context.

### references/deployment.md

```markdown
# Deployment Methods

## Git Integration

Automatic deployments from GitHub or GitLab repositories.

### Setup

1. Go to Workers & Pages > Create application > Pages
2. Connect to Git (GitHub or GitLab)
3. Configure:
   - Project name
   - Production branch
   - Build command
   - Build output directory
   - Root directory (for monorepos)

### Features

- **Preview deployments** — Automatic for non-production branches
- **PR previews** — Preview URLs posted to pull requests
- **Build status checks** — GitHub/GitLab commit status updates
- **Skip builds** — Include `[skip ci]` or `[ci skip]` in commit message

### Branch Controls

| Setting           | Options                      |
| ----------------- | ---------------------------- |
| Production branch | Single branch (e.g., `main`) |
| Preview branches  | All / None / Custom pattern  |

### Custom Branch Pattern

```
# Include
feature/*
release-*

# Exclude
dependabot/*
```

### Key Limitation

> **Cannot switch from Git integration to Direct Upload** — create new project if needed.

---

## Direct Upload

Upload prebuilt assets without Git connection.

### Wrangler CLI

```bash
# Create project
npx wrangler pages project create my-project

# Deploy to production
npx wrangler pages deploy ./dist

# Deploy preview (branch)
npx wrangler pages deploy ./dist --branch=staging

# List projects
npx wrangler pages project list

# List deployments
npx wrangler pages deployment list
```

### Dashboard Upload

- Drag and drop in Cloudflare dashboard
- Limited to 1,000 files, 25 MiB per file

### Wrangler Limits

- 20,000 files per deployment
- 25 MiB per file

---

## C3 CLI (create-cloudflare)

Scaffold new projects with framework support:

```bash
npm create cloudflare@latest -- --platform=pages
```

Options:

- `--framework` — Specify framework (react, vue, svelte, etc.)
- `--ts` — Use TypeScript
- `--git` — Initialize Git repository

---

## Deployment Comparison

| Feature                 | Git Integration | Direct Upload        |
| ----------------------- | --------------- | -------------------- |
| Auto-deploy on push     | ✅              | ❌                   |
| Preview deployments     | ✅ Automatic    | ✅ Manual (--branch) |
| CI/CD integration       | Built-in        | External CI required |
| Monorepo support        | ✅ (V2 build)   | ✅                   |
| Max files               | Unlimited       | 20,000 (Wrangler)    |
| Convert between methods | ❌              | ❌                   |

---

## Preview Deployments

### URL Pattern

```
<branch>.<project>.pages.dev
```

Branch name normalization:

- Lowercase
- Non-alphanumeric → hyphen

Example: `feature/user-auth` → `feature-user-auth.my-project.pages.dev`

### Robots Tag

Preview deployments include `X-Robots-Tag: noindex` by default.

### Access Control

Restrict preview access with Cloudflare Access policies.

---

## Rollbacks

Instantly revert to previous deployment:

1. Go to project > Deployments
2. Find deployment to restore
3. Click "Rollback to this deployment"

> Rollback creates a new deployment, preserving history.

```

### references/build.md

```markdown
# Build Configuration

## Build Commands

Specify how your site is built.

### Exit Codes

- `0` — Success, proceed to deployment
- Non-zero — Build failure

### Framework Presets

| Framework               | Build Command                     | Output Directory         |
| ----------------------- | --------------------------------- | ------------------------ |
| React (Vite)            | `npm run build`                   | `dist`                   |
| Next.js                 | `npx @cloudflare/next-on-pages@1` | `.vercel/output/static`  |
| Next.js (Static Export) | `npx next build`                  | `out`                    |
| Nuxt.js                 | `npm run build`                   | `dist`                   |
| SvelteKit               | `npm run build`                   | `.svelte-kit/cloudflare` |
| Astro                   | `npm run build`                   | `dist`                   |
| Vue (Vite)              | `npm run build`                   | `dist`                   |
| Remix                   | `npm run build`                   | `build/client`           |
| Gatsby                  | `npx gatsby build`                | `public`                 |
| Hugo                    | `hugo`                            | `public`                 |
| Jekyll                  | `jekyll build`                    | `_site`                  |
| Eleventy                | `npx @11ty/eleventy`              | `_site`                  |
| Zola                    | `zola build`                      | `public`                 |

### No Build Required

Use `exit 0` if project doesn't need building:

```
Build command: exit 0
```

---

## Environment Variables

### System Variables (auto-injected)

| Variable              | Description         |
| --------------------- | ------------------- |
| `CI`                  | Always `true`       |
| `CF_PAGES`            | Always `1`          |
| `CF_PAGES_BRANCH`     | Current branch name |
| `CF_PAGES_COMMIT_SHA` | Git commit SHA      |
| `CF_PAGES_URL`        | Deployment URL      |

### Setting Variables

**Dashboard:**

Workers & Pages > Project > Settings > Environment variables

**Local Development:**

```bash
# .dev.vars (do not commit)
API_KEY=secret123
DATABASE_URL=postgres://...
```

### Branch-Conditional Builds

```bash
#!/bin/bash
if [ "$CF_PAGES_BRANCH" == "production" ]; then
  npm run build:prod
elif [ "$CF_PAGES_BRANCH" == "staging" ]; then
  npm run build:staging
else
  npm run build:dev
fi
```

---

## Build Caching

Speeds up subsequent builds by caching:

- Package manager caches (npm, yarn, pnpm)
- Framework caches (`.next/cache`, `node_modules/.astro`, `.cache`)

### Requirements

- V2 build system or later
- Enable in project settings

### Limits

- Cache retention: 7 days after last read
- Per-project limit: 10 GB

---

## Node.js Version

### Set via Environment Variable

```
NODE_VERSION = 20
```

or specific:

```
NODE_VERSION = 20.10.0
```

### Set via Version File

Create `.nvmrc` or `.node-version` in project root:

```
20
```

### Default Versions

| Build System | Default Node.js |
| ------------ | --------------- |
| v3           | 22.16.0         |
| v2           | 18.17.1         |
| v1           | 12.18.0         |

---

## Build Watch Paths

Control which file changes trigger builds.

### Configuration

| Field   | Purpose                                |
| ------- | -------------------------------------- |
| Include | Only build if matched files changed    |
| Exclude | Skip build if only these files changed |

### Evaluation Order

1. Check excludes
2. Check includes
3. If any match → trigger build

### Wildcards

```
# Match all in directory
project-a/*

# Match specific extensions
*.md
src/**/*.test.ts
```

### Bypass Conditions

Build watch paths bypassed when:

- Push has 0 file changes
- Push has 3000+ file changes
- Push has 20+ commits

---

## Monorepo Support

Multiple Pages projects from same repository.

### Requirements

- V2 build system or later
- Up to 5 Pages projects per repository

### Configuration

1. Set root directory for each project
2. Use build watch paths to trigger correct project
3. Configure separate environment variables per project

---

## Build System Migration

### V3 (Current)

- Ubuntu 22.04.2
- Node.js 22.16.0 default
- Latest tooling

### V2

- Ubuntu 22.04.2
- Node.js 18.17.1 default
- Monorepo support

### V1 (Deprecated)

- Ubuntu 20.04.5
- Node.js 12.18.0 default

### Skip Dependency Install

```
SKIP_DEPENDENCY_INSTALL = true
```

Then run custom install in build command.

```

### references/functions.md

```markdown
# Pages Functions

Server-side code running on Cloudflare Workers runtime.

## Getting Started

Create a `/functions` directory at project root:

```
my-project/
├── public/          # Static assets
├── functions/       # Server-side code
│   ├── index.js
│   └── api/
│       └── hello.js
└── package.json
```

## File-Based Routing

| File Path                 | URL          |
| ------------------------- | ------------ |
| `/functions/index.js`     | `/`          |
| `/functions/about.js`     | `/about`     |
| `/functions/api/users.js` | `/api/users` |

### Dynamic Routes

**Single segment** (`[param]`):

```
/functions/users/[id].js → /users/123
```

```javascript
export function onRequest(context) {
  const { id } = context.params; // "123"
  return new Response(`User: ${id}`);
}
```

**Catch-all** (`[[catchall]]`):

```
/functions/files/[[path]].js → /files/a/b/c
```

```javascript
export function onRequest(context) {
  const segments = context.params.path; // ["a", "b", "c"]
  return new Response(`Path: ${segments.join("/")}`);
}
```

---

## Handlers

### Generic Handler

```javascript
export function onRequest(context) {
  return new Response("Handles all methods");
}
```

### Method-Specific Handlers

```javascript
export function onRequestGet(context) {
  return new Response("GET only");
}

export function onRequestPost(context) {
  return new Response("POST only");
}

export function onRequestPut(context) {
  /* ... */
}
export function onRequestDelete(context) {
  /* ... */
}
export function onRequestPatch(context) {
  /* ... */
}
export function onRequestHead(context) {
  /* ... */
}
export function onRequestOptions(context) {
  /* ... */
}
```

---

## Event Context

```typescript
interface EventContext<Env, Params, Data> {
  request: Request; // Incoming request
  functionPath: string; // Matched function path
  env: Env; // Bindings (KV, R2, D1, etc.)
  params: Params; // Route parameters
  data: Data; // Shared data from middleware
  waitUntil(promise: Promise<any>): void; // Background work
  passThroughOnException(): void; // Fall through on error
  next(input?: Request | string, init?: RequestInit): Promise<Response>;
}
```

---

## Middleware

Create `_middleware.js` in any functions directory:

```javascript
// functions/_middleware.js
export async function onRequest(context) {
  // Before handler
  console.log("Request:", context.request.url);

  // Call next handler
  const response = await context.next();

  // After handler
  response.headers.set("X-Custom-Header", "value");
  return response;
}
```

### Middleware Chain

Export array for multiple middleware:

```javascript
async function auth(context) {
  // Check auth
  return context.next();
}

async function logging(context) {
  console.log(context.request.url);
  return context.next();
}

export const onRequest = [auth, logging];
```

### Middleware Scope

- `functions/_middleware.js` — Applies to all routes
- `functions/api/_middleware.js` — Applies to `/api/*` routes

---

## TypeScript

### Setup

```bash
npx wrangler types
```

Creates `worker-configuration.d.ts` with Env types.

### tsconfig.json

```json
{
  "compilerOptions": {
    "target": "ES2021",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "types": ["@cloudflare/workers-types"]
  },
  "include": ["functions/**/*"]
}
```

### Typed Handler

```typescript
// functions/api/users.ts
interface Env {
  MY_KV: KVNamespace;
  MY_DB: D1Database;
}

export const onRequestGet: PagesFunction<Env> = async (context) => {
  const data = await context.env.MY_KV.get("key");
  return new Response(data);
};
```

---

## Invocation Control (\_routes.json)

Control when Functions are invoked to reduce charges:

```json
{
  "version": 1,
  "include": ["/api/*", "/admin/*"],
  "exclude": ["/static/*", "/*.ico"]
}
```

Place in build output directory (e.g., `dist/_routes.json`).

### Evaluation Order

1. Check excludes first
2. Then check includes
3. If matched → invoke Function
4. If not matched → serve static asset (no Function charge)

---

## Advanced Mode (\_worker.js)

Take full control with single Worker file:

```javascript
// dist/_worker.js
export default {
  async fetch(request, env) {
    const url = new URL(request.url);

    if (url.pathname.startsWith("/api/")) {
      return new Response(JSON.stringify({ ok: true }), {
        headers: { "Content-Type": "application/json" },
      });
    }

    // Serve static assets
    return env.ASSETS.fetch(request);
  },
};
```

> Advanced mode disables file-based routing.

---

## Module Support

### ES Modules

```javascript
import { helper } from "./utils.js";
```

### WebAssembly

```javascript
import wasm from "./module.wasm";
const instance = await WebAssembly.instantiate(wasm);
```

### Text/Binary

```javascript
import text from "./data.txt";
import binary from "./file.bin";
```

---

## Debugging

### Wrangler Tail

```bash
wrangler pages deployment tail
```

### Console Logging

```javascript
export function onRequest(context) {
  console.log("Request URL:", context.request.url);
  console.log("Headers:", Object.fromEntries(context.request.headers));
  return new Response("OK");
}
```

### Source Maps

```bash
npx wrangler pages deploy --upload-source-maps
```

Or in `wrangler.toml`:

```toml
upload_source_maps = true
```

---

## Pricing

- Functions requests billed as Workers requests
- Static asset requests (not invoking Functions) are free
- Use `_routes.json` to optimize costs

```

### references/bindings.md

```markdown
# Pages Functions Bindings

Connect Functions to Cloudflare resources via `context.env`.

## Supported Bindings

| Binding               | Type                     | Local Dev Support |
| --------------------- | ------------------------ | ----------------- |
| KV Namespace          | `KVNamespace`            | ✅                |
| R2 Bucket             | `R2Bucket`               | ✅                |
| D1 Database           | `D1Database`             | ✅                |
| Durable Objects       | `DurableObjectNamespace` | ✅                |
| Service Binding       | `Fetcher`                | ✅                |
| Queues Producer       | `Queue`                  | ✅                |
| Workers AI            | `Ai`                     | ❌ Remote only    |
| Vectorize             | `VectorizeIndex`         | ❌ Remote only    |
| Analytics Engine      | `AnalyticsEngineDataset` | ❌                |
| Hyperdrive            | `Hyperdrive`             | ❌                |
| Environment Variables | `string`                 | ✅                |
| Secrets               | `string`                 | ✅                |

---

## Configuration Methods

### Dashboard

1. Workers & Pages > Project > Settings > Bindings
2. Add binding (select type, provide name/ID)
3. Redeploy for changes to take effect

### Wrangler Configuration

```jsonc
// wrangler.jsonc
{
  "name": "my-pages-app",
  "pages_build_output_dir": "./dist",
  "compatibility_date": "2024-01-01",

  "kv_namespaces": [{ "binding": "MY_KV", "id": "abc123" }],

  "r2_buckets": [{ "binding": "MY_BUCKET", "bucket_name": "my-bucket" }],

  "d1_databases": [{ "binding": "MY_DB", "database_name": "my-db", "database_id": "xyz789" }],

  "services": [{ "binding": "AUTH_SERVICE", "service": "auth-worker" }],

  "ai": { "binding": "AI" },

  "vars": {
    "API_URL": "https://api.example.com",
    "DEBUG": "false"
  }
}
```

---

## Local Development

### Wrangler CLI Flags

```bash
npx wrangler pages dev ./dist \
  --kv=MY_KV \
  --r2=MY_BUCKET \
  --d1=MY_DB=<database-id> \
  --service=AUTH_SERVICE=auth-worker \
  --ai=AI \
  --binding=API_URL=https://api.example.com
```

### Local Secrets (.dev.vars)

```bash
# .dev.vars (do not commit!)
API_SECRET=secret123
DATABASE_URL=postgres://localhost/db
```

---

## Usage Examples

### KV Namespace

```javascript
export async function onRequestGet(context) {
  // Read
  const value = await context.env.MY_KV.get("key");
  const json = await context.env.MY_KV.get("key", { type: "json" });

  // Write
  await context.env.MY_KV.put("key", "value", {
    expirationTtl: 3600,
    metadata: { created: Date.now() },
  });

  // Delete
  await context.env.MY_KV.delete("key");

  // List
  const { keys } = await context.env.MY_KV.list({ prefix: "user:" });

  return new Response(JSON.stringify(keys));
}
```

> For complete KV API, see: `cloudflare-kv` skill

### R2 Bucket

```javascript
export async function onRequest(context) {
  // Upload
  await context.env.MY_BUCKET.put("file.txt", "content", {
    httpMetadata: { contentType: "text/plain" },
  });

  // Download
  const object = await context.env.MY_BUCKET.get("file.txt");
  if (!object) {
    return new Response("Not found", { status: 404 });
  }

  return new Response(object.body, {
    headers: {
      "Content-Type": object.httpMetadata?.contentType || "application/octet-stream",
    },
  });
}
```

> For complete R2 API, see: `cloudflare-r2` skill

### D1 Database

```javascript
export async function onRequestGet(context) {
  const { results } = await context.env.MY_DB.prepare("SELECT * FROM users WHERE id = ?").bind(context.params.id).all();

  return Response.json(results);
}

export async function onRequestPost(context) {
  const body = await context.request.json();

  const result = await context.env.MY_DB.prepare("INSERT INTO users (name, email) VALUES (?, ?)").bind(body.name, body.email).run();

  return Response.json({ id: result.meta.last_row_id });
}
```

> For complete D1 API, see: `cloudflare-d1` skill

### Workers AI

```javascript
export async function onRequestPost(context) {
  const body = await context.request.json();

  const response = await context.env.AI.run("@cf/meta/llama-3-8b-instruct", {
    messages: [{ role: "user", content: body.prompt }],
  });

  return Response.json(response);
}
```

> **Note:** Workers AI does not work locally — always uses remote API (incurs charges).

### Service Binding

```javascript
export async function onRequest(context) {
  // Call another Worker
  const authResponse = await context.env.AUTH_SERVICE.fetch(
    new Request("https://auth/verify", {
      headers: { Authorization: context.request.headers.get("Authorization") },
    })
  );

  if (!authResponse.ok) {
    return new Response("Unauthorized", { status: 401 });
  }

  return new Response("Protected content");
}
```

### Durable Objects

```javascript
export async function onRequest(context) {
  const id = context.env.COUNTER.idFromName("global");
  const stub = context.env.COUNTER.get(id);

  const response = await stub.fetch(context.request);
  return response;
}
```

> Durable Objects must be defined in a separate Worker and bound to Pages project.

### Queues Producer

```javascript
export async function onRequestPost(context) {
  const body = await context.request.json();

  await context.env.MY_QUEUE.send({
    type: "process",
    data: body,
  });

  return new Response("Queued", { status: 202 });
}
```

### Vectorize

```javascript
export async function onRequestPost(context) {
  const body = await context.request.json();

  // Query similar vectors
  const results = await context.env.VECTORIZE_INDEX.query(body.vector, {
    topK: 5,
    returnMetadata: true,
  });

  return Response.json(results);
}
```

### Environment Variables

```javascript
export function onRequest(context) {
  const apiUrl = context.env.API_URL;
  const debug = context.env.DEBUG === "true";

  return new Response(`API: ${apiUrl}, Debug: ${debug}`);
}
```

---

## Binding Environments

Configure different bindings for preview vs production:

```jsonc
// wrangler.jsonc
{
  "name": "my-app",
  "pages_build_output_dir": "./dist",

  "d1_databases": [{ "binding": "DB", "database_name": "prod-db", "database_id": "prod-id" }],

  "env": {
    "preview": {
      "d1_databases": [{ "binding": "DB", "database_name": "dev-db", "database_id": "dev-id" }]
    }
  }
}
```

> **Important:** When overriding any non-inheritable key in `env.preview` or `env.production`, you must specify ALL non-inheritable keys in that block.

```

### references/headers-redirects.md

```markdown
# Headers and Redirects

Static configuration files for HTTP headers and URL redirects.

## \_headers File

Create `_headers` in build output directory (e.g., `public/_headers` or `dist/_headers`).

### Syntax

```txt
[url-pattern]
  [Header-Name]: [value]
```

### Examples

```txt
# Apply to all pages
/*
  X-Frame-Options: DENY
  X-Content-Type-Options: nosniff
  Referrer-Policy: strict-origin-when-cross-origin

# Specific path
/secure/*
  Content-Security-Policy: default-src 'self'

# Cache static assets
/static/*
  Cache-Control: public, max-age=31536000, immutable

# API endpoints
/api/*
  Access-Control-Allow-Origin: *
  Access-Control-Allow-Methods: GET, POST, PUT, DELETE
```

### Placeholders and Splats

```txt
# Named placeholder
/users/:id
  X-User-Id: :id

# Splat (catch-all)
/files/*
  X-Path: :splat
```

### Detach Headers

Remove inherited headers with `! `:

```txt
# Remove header for specific path
/public/*
  ! Content-Security-Policy
```

### Limits

| Limit             | Value             |
| ----------------- | ----------------- |
| Max rules         | 100               |
| Max line length   | 2,000 characters  |
| Duplicate headers | Joined with comma |

---

## \_redirects File

Create `_redirects` in build output directory.

### Syntax

```txt
[source] [destination] [status-code]
```

Default status code: 302 (temporary redirect)

### Examples

```txt
# Permanent redirect
/old-page /new-page 301

# Temporary redirect (default)
/temp /other

# External redirect
/twitter https://twitter.com/myaccount 301

# Trailing slash normalization
/about/ /about 301
/contact /contact/ 301

# Fragment redirect
/section /page#anchor 301
```

### Splats and Placeholders

```txt
# Splat (catch-all)
/blog/* https://blog.example.com/:splat

# Named placeholder
/users/:id /profiles/:id 301

# Multiple placeholders
/products/:category/:id /shop/:category/:id 301
```

### Proxying (200 Status)

Proxy requests to different path (relative URLs only):

```txt
# Proxy /api to /backend
/api/* /backend/:splat 200

# SPA fallback
/* /index.html 200
```

> **Limitation:** Cannot proxy to absolute URLs.

### Limits

| Limit             | Value            |
| ----------------- | ---------------- |
| Static redirects  | 2,000            |
| Dynamic redirects | 100              |
| Total redirects   | 2,100            |
| Max line length   | 1,000 characters |

### Unsupported Features

- Query string matching
- Domain-level redirects
- Country/language redirects
- Cookie-based redirects
- Absolute URL proxying

---

## Execution Order

1. Redirects execute first
2. Headers applied after redirects

---

## Early Hints (103)

Automatic `Link` header generation for preloading resources.

### Automatic Generation

Pages automatically creates `Link` headers from HTML:

```html
<link rel="preload" href="/app.js" as="script" />
<link rel="preconnect" href="https://api.example.com" />
<link rel="modulepreload" href="/module.js" />
```

### Manual Link Headers

```txt
/*
  Link: </app.js>; rel=preload; as=script
  Link: </styles.css>; rel=preload; as=style
```

### Disable Automatic Links

```txt
/*
  ! Link
```

---

## Pages Functions Warning

> **Critical:** `_headers` and `_redirects` do NOT apply to responses from Pages Functions.

For Functions responses, attach headers in code:

```javascript
export function onRequest(context) {
  return new Response("Hello", {
    headers: {
      "X-Custom-Header": "value",
      "Cache-Control": "public, max-age=3600",
    },
  });
}
```

For redirects in Functions:

```javascript
export function onRequest(context) {
  return Response.redirect("https://example.com/new-path", 301);
}
```

---

## Default Response Headers

Pages automatically adds:

| Header                        | Value                             |
| ----------------------------- | --------------------------------- |
| `Access-Control-Allow-Origin` | `*`                               |
| `Referrer-Policy`             | `strict-origin-when-cross-origin` |
| `X-Content-Type-Options`      | `nosniff`                         |
| `Server`                      | `cloudflare`                      |
| `Cf-Ray`                      | Request ID                        |
| `Etag`                        | Content hash                      |

Preview deployments also add:

| Header         | Value     |
| -------------- | --------- |
| `X-Robots-Tag` | `noindex` |

---

## Bulk Redirects

For exceeding `_redirects` limits, use account-level Bulk Redirects:

1. Rules > Bulk Redirects
2. Create redirect list
3. Enable list

> Bulk Redirects apply at account level, not project level.

```

### references/domains.md

```markdown
# Custom Domains

Configure custom domains for Pages projects.

## Adding a Custom Domain

### Dashboard Method

1. Workers & Pages > Project > Custom domains
2. Set up a domain
3. Enter domain name
4. Follow activation steps

### Domain Types

| Type                          | Requirements                                       |
| ----------------------------- | -------------------------------------------------- |
| Apex domain (`example.com`)   | Must add zone to Cloudflare, configure nameservers |
| Subdomain (`app.example.com`) | CNAME record only (zone optional)                  |

---

## DNS Configuration

### Subdomain (Recommended)

Add CNAME record in your DNS provider:

| Type  | Name  | Content               |
| ----- | ----- | --------------------- |
| CNAME | `app` | `<project>.pages.dev` |

Example:

```
shop.example.com → my-store.pages.dev
```

### Apex Domain

1. Add domain to Cloudflare as a zone
2. Update nameservers at registrar to Cloudflare
3. CNAME record added automatically

---

## SSL/TLS

- Automatic SSL certificate issuance
- Edge Certificates enabled by default
- HTTPS enforced

### CAA Records

If you have CAA records, add:

```
example.com. CAA 0 issue "digicert.com"
example.com. CAA 0 issue "letsencrypt.org"
example.com. CAA 0 issue "pki.goog"
```

> Missing CAA records may block certificate issuance.

---

## Multiple Domains

Attach multiple custom domains to same project:

1. Primary domain: `www.example.com`
2. Redirect domain: `example.com` → redirect to `www.example.com`

---

## Removing Custom Domain

1. Delete CNAME record from DNS zone
2. Workers & Pages > Project > Custom domains
3. Remove domain from project

---

## Disable pages.dev Access

Prevent access to `*.pages.dev` subdomain:

### Method 1: Cloudflare Access

Apply Access policy to pages.dev subdomain.

### Method 2: Bulk Redirects

Create account-level Bulk Redirect:

```
<project>.pages.dev/* → https://example.com/:splat 301
```

---

## Preview Subdomain Access

Preview deployments use pattern:

```
<branch>.<project>.pages.dev
```

### Restrict Preview Access

1. Apply Cloudflare Access policy
2. Configure allowed users/groups

---

## Caching

### Default Behavior

- Pages handles caching automatically
- Assets served from Tiered Cache
- No additional configuration needed

### Custom Domain Caching

When using custom domain via Cloudflare zone:

- Zone caching rules may apply
- Avoid conflicting cache settings

### Purge Cache

After deployment, stale assets may persist:

1. Go to zone Caching settings
2. Purge Everything

> Deploy triggers automatic cache invalidation for most assets.

---

## Known Issues

### Domain Inactive After DNS Change

If domain was temporarily pointed elsewhere:

1. Domain may become inactive
2. Re-validate in project settings
3. Or use Origin Rules for temporary redirects

### Conflicting Zone Settings

Page Rules or other zone settings may override Pages behavior:

- Disable conflicting Page Rules
- Review zone caching configuration
- Check redirect rules

---

## Domain Verification

For new domains:

1. Add TXT record if required for verification
2. Wait for DNS propagation (up to 48 hours)
3. Check domain status in dashboard

---

## Troubleshooting

| Issue                   | Solution                                     |
| ----------------------- | -------------------------------------------- |
| SSL certificate pending | Check CAA records, wait for issuance         |
| Domain not resolving    | Verify CNAME points to `<project>.pages.dev` |
| Redirect loop           | Check conflicting redirects in zone          |
| 522/523 errors          | Verify project is deployed and healthy       |

```

### references/wrangler.md

```markdown
# Wrangler CLI for Pages

Command-line interface for Pages development and deployment.

## Installation

```bash
npm install -g wrangler
# or
npm install --save-dev wrangler
```

## Project Commands

### Create Project

```bash
npx wrangler pages project create <project-name>
```

### List Projects

```bash
npx wrangler pages project list
```

### Delete Project

```bash
npx wrangler pages project delete <project-name>
```

---

## Deployment Commands

### Deploy

```bash
# Deploy to production
npx wrangler pages deploy <build-output-dir>

# Deploy to branch (preview)
npx wrangler pages deploy <build-output-dir> --branch=staging

# With commit message
npx wrangler pages deploy <build-output-dir> --commit-message="Deploy v1.2.3"
```

### List Deployments

```bash
npx wrangler pages deployment list
```

### Tail Logs

```bash
npx wrangler pages deployment tail
```

---

## Local Development

### Start Dev Server

```bash
npx wrangler pages dev <build-output-dir>
```

Default: `http://localhost:8788`

### Options

| Flag                     | Description             |
| ------------------------ | ----------------------- |
| `--port`                 | Custom port             |
| `--local-protocol=https` | Use HTTPS               |
| `--compatibility-date`   | Set compatibility date  |
| `--compatibility-flags`  | Set compatibility flags |

### With Bindings

```bash
npx wrangler pages dev ./dist \
  --kv=MY_KV \
  --r2=MY_BUCKET \
  --d1=MY_DB=<database-id> \
  --service=AUTH=auth-worker \
  --ai=AI \
  --do=COUNTER=CounterClass@counter-worker \
  --binding=API_URL=https://api.example.com
```

### Keyboard Shortcuts

| Key | Action        |
| --- | ------------- |
| `b` | Open browser  |
| `d` | Open DevTools |
| `c` | Clear console |
| `x` | Exit          |

---

## Wrangler Configuration

### Minimal wrangler.jsonc

```jsonc
{
  "$schema": "./node_modules/wrangler/config-schema.json",
  "name": "my-pages-app",
  "pages_build_output_dir": "./dist",
  "compatibility_date": "2024-01-01"
}
```

### Full Configuration

```jsonc
{
  "$schema": "./node_modules/wrangler/config-schema.json",
  "name": "my-pages-app",
  "pages_build_output_dir": "./dist",
  "compatibility_date": "2024-01-01",
  "compatibility_flags": ["nodejs_compat"],

  "kv_namespaces": [{ "binding": "KV", "id": "abc123" }],

  "r2_buckets": [{ "binding": "BUCKET", "bucket_name": "my-bucket" }],

  "d1_databases": [{ "binding": "DB", "database_name": "my-db", "database_id": "xyz789" }],

  "services": [{ "binding": "AUTH", "service": "auth-worker" }],

  "durable_objects": {
    "bindings": [{ "name": "COUNTER", "class_name": "Counter", "script_name": "counter-worker" }]
  },

  "queues": {
    "producers": [{ "binding": "QUEUE", "queue": "my-queue" }]
  },

  "ai": { "binding": "AI" },

  "vectorize": [{ "binding": "VECTORIZE", "index_name": "my-index" }],

  "analytics_engine_datasets": [{ "binding": "ANALYTICS", "dataset": "my-dataset" }],

  "vars": {
    "API_URL": "https://api.example.com",
    "DEBUG": "false"
  },

  "upload_source_maps": true
}
```

### Environment Overrides

```jsonc
{
  "name": "my-app",
  "pages_build_output_dir": "./dist",

  "d1_databases": [{ "binding": "DB", "database_name": "prod-db", "database_id": "prod-id" }],

  "env": {
    "preview": {
      "d1_databases": [{ "binding": "DB", "database_name": "dev-db", "database_id": "dev-id" }]
    }
  }
}
```

---

## Download Existing Config

Generate `wrangler.toml` from existing dashboard configuration:

```bash
npx wrangler pages download config <project-name>
```

---

## Type Generation

Generate TypeScript types for bindings:

```bash
npx wrangler types
```

Creates `worker-configuration.d.ts`:

```typescript
interface Env {
  MY_KV: KVNamespace;
  MY_BUCKET: R2Bucket;
  MY_DB: D1Database;
  AI: Ai;
  API_URL: string;
}
```

---

## Source Maps

Upload source maps for debugging:

```bash
npx wrangler pages deploy --upload-source-maps
```

Or in configuration:

```jsonc
{
  "upload_source_maps": true
}
```

Requires Wrangler >= 3.60.0

---

## Requirements

| Feature              | Requirement         |
| -------------------- | ------------------- |
| Wrangler config file | Wrangler >= 3.45.0  |
| V2 build system      | For Wrangler config |
| Source maps          | Wrangler >= 3.60.0  |

---

## Wrangler vs Dashboard

When using Wrangler configuration:

| Setting               | Source of Truth |
| --------------------- | --------------- |
| Bindings              | Wrangler file   |
| Environment variables | Wrangler file   |
| Build settings        | Dashboard       |
| Git integration       | Dashboard       |
| Custom domains        | Dashboard       |

> **Important:** Do not edit bindings in dashboard when using Wrangler file — changes may be overwritten.

---

## Common Issues

| Issue                      | Solution                                         |
| -------------------------- | ------------------------------------------------ |
| "Project not found"        | Create project first with `pages project create` |
| Local bindings not working | Add binding flags to `pages dev`                 |
| Types not generated        | Run `wrangler types` after adding bindings       |
| Deploy fails               | Check build output directory exists              |

```

cloudflare-pages | SkillHub