| name | cloudflare-pages |
| version | 1.0.0 |
| description | Cloudflare Pages - Full-stack JAMstack platform with global CDN, serverless functions, Git integration, and native bindings to Workers, KV, R2, D1, and Durable Objects |
Cloudflare Pages Skill
Cloudflare Pages is a JAMstack platform for deploying full-stack applications to Cloudflare's global network. It combines static site hosting with serverless Pages Functions, automatic Git deployments, and native integrations with the Cloudflare ecosystem (Workers, KV, R2, D1, Durable Objects).
Key Value Proposition: Deploy static sites and full-stack applications with zero configuration, automatic preview deployments, unlimited bandwidth, and seamless access to Cloudflare's edge computing and storage services.
When to Use This Skill
- Deploying static sites or JAMstack applications
- Building full-stack apps with serverless functions
- Configuring Pages Functions with bindings (KV, R2, D1)
- Setting up Git-based CI/CD with preview deployments
- Configuring headers, redirects, and routing
- Troubleshooting build or deployment issues
- Migrating from other hosting platforms to Pages
When NOT to Use This Skill
- For Cloudflare Workers alone (use workers skill)
- For general Cloudflare CDN/DNS configuration
- For non-web applications or APIs without UI
- For container-based deployments (Pages is serverless)
Core Concepts
Architecture Overview
┌─────────────────────────────────────────────────────────────────┐
│ Cloudflare Pages │
│ Global Edge Network (300+ PoPs) │
└─────────────────────────────────────────────────────────────────┘
│
┌─────────────────────┼─────────────────────┐
│ │ │
▼ ▼ ▼
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ Static Assets │ │ Pages Functions│ │ Bindings │
├───────────────┤ ├───────────────┤ ├───────────────┤
│ • HTML/CSS/JS │ │ • /functions/ │ │ • KV │
│ • Images │ │ • Middleware │ │ • R2 │
│ • _headers │ │ • Routing │ │ • D1 │
│ • _redirects │ │ • TypeScript │ │ • DO │
│ • _routes.json│ │ • Workers API │ │ • Queues │
└───────────────┘ └───────────────┘ │ • AI │
│ │ └───────────────┘
└─────────────────────┼─────────────────────┘
│
┌─────────────────────┼─────────────────────┐
▼ ▼ ▼
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ Deployment │ │ Domains │ │ Builds │
├───────────────┤ ├───────────────┤ ├───────────────┤
│ • Git (GH/GL) │ │ • Custom │ │ • 20 min max │
│ • Direct │ │ • *.pages.dev │ │ • 500/mo free │
│ • Wrangler │ │ • Preview URLs│ │ • Env vars │
│ • API │ │ │ │ • Presets │
└───────────────┘ └───────────────┘ └───────────────┘
Platform Limits
| Resource | Free | Pro | Business | Enterprise |
|---|
| Builds/month | 500 | 5,000 | 20,000 | Unlimited |
| Custom domains | 100 | 250 | 500 | 500 |
| Files per site | 20,000 | 20,000 | 20,000 | 20,000 |
| File size | 25 MB | 25 MB | 25 MB | 25 MB |
| Preview deployments | Unlimited | Unlimited | Unlimited | Unlimited |
| Build timeout | 20 min | 20 min | 20 min | 20 min |
Configuration Limits
| Config | Limit |
|---|
| Header rules | 100 max |
| Static redirects | 2,000 max |
| Dynamic redirects | 100 max |
| Redirect line length | 1,000 chars |
| Header line length | 2,000 chars |
| Projects per account | 100 (soft) |
Quick Start
Deploy with Git Integration
git push origin main
Deploy with Wrangler CLI
npm install -g wrangler
wrangler login
npx wrangler pages project create my-site
npx wrangler pages deploy ./dist
npx wrangler pages deploy ./dist --branch=staging
Deploy via Dashboard
- Go to Workers & Pages > Create application
- Click Get started under Pages
- Drag and drop your build folder or zip file
- Enter project name and deploy
Build Configuration
Framework Presets
Cloudflare provides presets for 31+ frameworks:
| Framework | Build Command | Output Directory |
|---|
| React (Vite) | npm run build | dist |
| Next.js | npx @cloudflare/next-on-pages@1 | .vercel/output/static |
| Astro | npm run build | dist |
| SvelteKit | npm run build | .svelte-kit/cloudflare |
| Nuxt | npm run build | dist |
| Remix | npm run build | public |
| Vue (Vite) | npm run build | dist |
| Angular | npm run build | dist/<project> |
| Gatsby | gatsby build | public |
| Hugo | hugo | public |
| Docusaurus | npm run build | build |
| Eleventy | npx @11ty/eleventy | _site |
System Environment Variables
These are automatically injected during builds:
| Variable | Description | Example |
|---|
CF_PAGES | Always 1 on Pages | 1 |
CF_PAGES_COMMIT_SHA | Git commit hash | a1b2c3d... |
CF_PAGES_BRANCH | Branch name | main |
CF_PAGES_URL | Deployment URL | https://abc.pages.dev |
CI | Always true | true |
Custom Environment Variables
[vars]
API_URL = "https://api.example.com"
Monorepo Configuration
my-monorepo/
├── apps/
│ └── web/ # Root directory: apps/web
│ ├── src/
│ └── package.json
└── packages/
Set Root directory to apps/web in build settings.
Pages Functions
Directory Structure
my-project/
├── functions/
│ ├── _middleware.js # Global middleware
│ ├── api/
│ │ ├── _middleware.js # API-specific middleware
│ │ ├── index.js # GET /api
│ │ ├── users/
│ │ │ ├── index.js # GET /api/users
│ │ │ └── [id].js # GET /api/users/:id
│ │ └── [[catchall]].js # Catch-all
│ └── health.js # GET /health
├── public/
│ └── index.html
└── package.json
Basic Function
export const onRequestGet: PagesFunction = async (context) => {
return new Response(JSON.stringify({ message: "Hello!" }), {
headers: { "Content-Type": "application/json" }
});
};
export const onRequest: PagesFunction = async (context) => {
const { request } = context;
if (request.method === "POST") {
const body = await request.json();
return new Response(JSON.stringify(body));
}
return new Response("Method not allowed", { status: 405 });
};
Dynamic Routes
export const onRequestGet: PagesFunction = async (context) => {
const { id } = context.params;
return new Response(`User: ${id}`);
};
export const onRequestGet: PagesFunction = async (context) => {
const { path } = context.params;
return new Response(`Path: ${path.join("/")}`);
};
Middleware
export const onRequest: PagesFunction = async (context) => {
console.log(`${context.request.method} ${context.request.url}`);
try {
const response = await context.next();
response.headers.set("X-Custom-Header", "value");
return response;
} catch (err) {
return new Response("Server Error", { status: 500 });
}
};
export const onRequest = [errorHandler, auth, logging];
async function errorHandler(context) {
try {
return await context.next();
} catch (err) {
return new Response(err.message, { status: 500 });
}
}
async function () {
token = context...();
(!token) {
(, { : });
}
context.();
}
TypeScript Support
interface Env {
KV: KVNamespace;
DB: D1Database;
BUCKET: R2Bucket;
API_KEY: string;
}
export const onRequestGet: PagesFunction<Env> = async (context) => {
const { env, request, params, waitUntil, passThroughOnException } = context;
const value = await env.KV.get("key");
const result = await env.DB.prepare("SELECT * FROM users").all();
return Response.json({ value, users: result.results });
};
Bindings
KV Namespace
interface Env {
MY_KV: KVNamespace;
}
export const onRequest: PagesFunction<Env> = async ({ env }) => {
await env.MY_KV.put("key", "value");
await env.MY_KV.put("json", JSON.stringify({ foo: "bar" }));
await env.MY_KV.put("expiring", "data", { expirationTtl: 3600 });
const value = await env.MY_KV.get("key");
const json = await env.MY_KV.get("json", { type: "json" });
const list = await env.MY_KV.list({ prefix: "user:" });
await env..();
.({ value, json });
};
npx wrangler pages dev ./dist --kv=MY_KV
R2 Bucket
interface Env {
BUCKET: R2Bucket;
}
export const onRequest: PagesFunction<Env> = async ({ env, request }) => {
const url = new URL(request.url);
const key = url.pathname.slice(1);
switch (request.method) {
case "PUT":
await env.BUCKET.put(key, request.body, {
httpMetadata: { contentType: request.headers.get("content-type") }
});
return new Response("Uploaded");
case "GET":
const object = await env.BUCKET.get(key);
if (!object) return new Response("Not found", { status: 404 });
return new (., {
: { : .?. || }
});
:
env..(key);
();
}
};
npx wrangler pages dev ./dist --r2=BUCKET
D1 Database
interface Env {
DB: D1Database;
}
export const onRequestGet: PagesFunction<Env> = async ({ env }) => {
const { results } = await env.DB
.prepare("SELECT * FROM users WHERE active = ?")
.bind(1)
.all();
return Response.json(results);
};
export const onRequestPost: PagesFunction<Env> = async ({ env, request }) => {
const { name, email } = await request.json();
const result = await env.DB
.prepare("INSERT INTO users (name, email) VALUES (?, ?)")
.bind(name, email)
.run();
return Response.json({ id: result.meta.last_row_id });
};
npx wrangler pages dev ./dist --d1=DB=<database-id>
Durable Objects
interface Env {
COUNTER: DurableObjectNamespace;
}
export const onRequest: PagesFunction<Env> = async ({ env, request }) => {
const id = env.COUNTER.idFromName("global");
const stub = env.COUNTER.get(id);
return stub.fetch(request);
};
npx wrangler pages dev ./dist --do=COUNTER=CounterDO@counter-worker
Service Bindings
interface Env {
AUTH_SERVICE: Fetcher;
}
export const onRequest: PagesFunction<Env> = async ({ env, request }) => {
return env.AUTH_SERVICE.fetch(request);
};
Queue Producer
interface Env {
MY_QUEUE: Queue;
}
export const onRequestPost: PagesFunction<Env> = async ({ env, request }) => {
const job = await request.json();
await env.MY_QUEUE.send({
type: "process",
data: job
});
return Response.json({ queued: true });
};
Workers AI
interface Env {
AI: Ai;
}
export const onRequestPost: PagesFunction<Env> = async ({ env, request }) => {
const { prompt } = await request.json();
const response = await env.AI.run("@cf/meta/llama-2-7b-chat-int8", {
messages: [{ role: "user", content: prompt }]
});
return Response.json(response);
};
Static Configuration
Headers (_headers)
# Apply to all paths
/*
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
# API CORS headers
/api/*
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type
# Cache static assets
/assets/*
Cache-Control: public, max-age=31536000, immutable
# Security headers for HTML
/*.html
Content-Security-Policy: default-src 'self'
X-XSS-Protection: 1; mode=block
# Remove header (prefix with !)
/public/*
! X-Robots-Tag
Note: Custom headers do NOT apply to Pages Functions responses.
Redirects (_redirects)
# Simple redirect (302 default)
/home /
# Permanent redirect
/old-page /new-page 301
# External redirect
/twitter https://twitter.com/example
# Splat (wildcard)
/blog/* https://blog.example.com/:splat
# Placeholder
/users/:id /profiles/:id 301
# Proxy (200 status, relative URLs only)
/api/* /functions/api/:splat 200
# Force trailing slash
/about /about/ 301
Limits: 2,000 static + 100 dynamic redirects max.
Routes (_routes.json)
{
"version": 1,
"include": ["/api/*", "/auth/*"],
"exclude": ["/api/health", "/static/*"]
}
- Controls which paths invoke Functions
exclude takes precedence over include
- Paths not matching either are served as static assets
- Max 100 rules combined
Deployment Methods
Git Integration (Recommended)
GitHub Setup:
- Workers & Pages > Create application > Connect to Git
- Select GitHub and authorize
- Choose repository
- Configure build settings
- Deploy
GitLab Setup:
- Requires Maintainer role or higher
- Grants access to all repositories on account
Branch Configuration:
- Production branch:
main (configurable)
- Preview branches: All non-production (configurable)
- Custom rules: Include/exclude patterns
Direct Upload (Wrangler)
npx wrangler pages project create my-app
npx wrangler pages project list
npx wrangler pages deploy ./dist
npx wrangler pages deploy ./dist --branch=feature-x
npx wrangler pages deployment list --project-name=my-app
API Deployment
curl -X POST \
"https://api.cloudflare.com/client/v4/accounts/{account_id}/pages/projects/{project_name}/deployments" \
-H "Authorization: Bearer {api_token}" \
-F "manifest=@manifest.json" \
-F "file1=@dist/index.html"
Local Development
Wrangler Dev Server
npx wrangler pages dev ./dist
npx wrangler pages dev ./dist \
--kv=MY_KV \
--r2=BUCKET \
--d1=DB=<database-id> \
--port=8788
npx wrangler pages dev -- npm run dev
npx wrangler pages dev --proxy=3000
wrangler.toml Configuration
name = "my-pages-project"
pages_build_output_dir = "./dist"
compatibility_date = "2024-01-01"
compatibility_flags = ["nodejs_compat"]
[vars]
API_URL = "https://api.example.com"
[[kv_namespaces]]
binding = "MY_KV"
id = "abc123"
[[r2_buckets]]
binding = "BUCKET"
bucket_name = "my-bucket"
[[d1_databases]]
binding = "DB"
database_name = "my-db"
database_id = "xyz789"
[[durable_objects.bindings]]
name = "COUNTER"
class_name = "Counter"
script_name = "counter-worker"
[[services]]
binding = "AUTH"
service = "auth-worker"
Branch Build Controls
Preview Deployments
Settings > Builds & deployments > Configure Preview deployments
Options:
- All non-production branches: Default, deploys everything
- None: Disables preview builds
- Custom branches: Selective deployment
Custom Branch Rules
# Include patterns
feat/* # Match feat/login, feat/dashboard
release-* # Match release-1.0, release-2.0
# Exclude patterns
dependabot/* # Skip dependency updates
wip/* # Skip work-in-progress
# Order: excludes evaluated first, then includes
Skip Builds
Add to commit message:
git commit -m "Update docs [CI SKIP]"
git commit -m "Minor fix [skip ci]"
Rollbacks
Via Dashboard
- Go to project > Deployments
- Find the deployment to rollback to
- Click three dots > Rollback to this deployment
Via Wrangler
npx wrangler pages deployment list --project-name=my-app
Troubleshooting
Build Failures
Error: Build command failed
Solutions:
- Check build logs for specific error
- Verify build command and output directory
- Test locally:
npm run build
- Check Node.js version compatibility
- Review environment variables
Function 500 Errors
Error: Internal Server Error
Solutions:
- Check function logs in dashboard
- Verify bindings are configured
- Test locally with
wrangler pages dev
- Check for unhandled promise rejections
Static Asset 404
Error: Page not found
Solutions:
- Verify build output directory is correct
- Check
_routes.json isn't excluding the path
- Confirm file exists in build output
- Review
_redirects for conflicts
Binding Not Found
Error: Cannot read property of undefined
Solutions:
- Verify binding is configured in dashboard
- Check binding name matches code
- For local dev, pass binding flags to wrangler
- Verify wrangler.toml configuration
Headers/Redirects Not Working
Possible causes:
- File not in correct location (must be in build output)
- Syntax errors in file
- For Functions responses, use code instead
- Exceeded limit (100 headers, 2100 redirects)
Best Practices
Project Structure
my-app/
├── functions/ # Server-side code
│ ├── _middleware.ts # Global middleware
│ └── api/ # API routes
├── public/ # Static assets (copied to dist)
│ ├── _headers # Header rules
│ ├── _redirects # Redirect rules
│ └── robots.txt
├── src/ # Source code
├── dist/ # Build output (gitignored)
├── wrangler.toml # Wrangler config
└── package.json
Security Recommendations
- Use environment variables for secrets
- Implement CORS properly for APIs
- Add security headers (CSP, X-Frame-Options)
- Validate and sanitize user input
- Use HTTPS-only custom domains
- Review function permissions and bindings
Performance Tips
- Use
_routes.json to skip Functions for static paths
- Set appropriate Cache-Control headers
- Optimize images before upload
- Use code splitting for large apps
- Leverage Cloudflare's global CDN
Resources
Official Documentation
Tools
Related Services
Community
Version History
- 1.0.0 (2026-01-13): Initial skill release
- Complete Pages platform documentation
- Static site and full-stack deployment guides
- Pages Functions with routing and middleware
- All bindings (KV, R2, D1, DO, Queues, AI)
- Build configuration and framework presets
- Headers, redirects, and routes configuration
- Git integration and direct upload methods
- Local development with Wrangler
- Branch build controls and rollbacks
- Troubleshooting guide