Claude-skill-registry cloudflare-pages
Cloudflare Pages - Full-stack JAMstack platform with global CDN, serverless functions, Git integration, and native bindings to Workers, KV, R2, D1, and Durable Objects
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/cloudflare-pages" ~/.claude/skills/majiayu000-claude-skill-registry-cloudflare-pages && rm -rf "$T"
skills/data/cloudflare-pages/SKILL.mdCloudflare 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
# 1. Connect repository in Cloudflare Dashboard # Workers & Pages > Create application > Connect to Git # 2. Configure build settings # - Production branch: main # - Build command: npm run build # - Build output: dist # 3. Push to deploy git push origin main
Deploy with Wrangler CLI
# Install Wrangler npm install -g wrangler # Login to Cloudflare wrangler login # Create a new project npx wrangler pages project create my-site # Deploy to production npx wrangler pages deploy ./dist # Deploy preview 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) | | |
| Next.js | | |
| Astro | | |
| SvelteKit | | |
| Nuxt | | |
| Remix | | |
| Vue (Vite) | | |
| Angular | | |
| Gatsby | | |
| Hugo | | |
| Docusaurus | | |
| Eleventy | | |
System Environment Variables
These are automatically injected during builds:
| Variable | Description | Example |
|---|---|---|
| Always on Pages | |
| Git commit hash | |
| Branch name | |
| Deployment URL | |
| Always | |
Custom Environment Variables
# Set via dashboard: Settings > Environment variables # Or in wrangler.toml [vars] API_URL = "https://api.example.com" # Secrets (encrypted) # Set via: wrangler pages secret put SECRET_NAME
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
// functions/api/hello.ts export const onRequestGet: PagesFunction = async (context) => { return new Response(JSON.stringify({ message: "Hello!" }), { headers: { "Content-Type": "application/json" } }); }; // Handle multiple methods 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
// functions/users/[id].ts export const onRequestGet: PagesFunction = async (context) => { const { id } = context.params; // string return new Response(`User: ${id}`); }; // functions/files/[[path]].ts (catch-all) export const onRequestGet: PagesFunction = async (context) => { const { path } = context.params; // string[] return new Response(`Path: ${path.join("/")}`); };
Middleware
// functions/_middleware.ts export const onRequest: PagesFunction = async (context) => { // Before handler console.log(`${context.request.method} ${context.request.url}`); try { // Call next handler const response = await context.next(); // After handler - modify response response.headers.set("X-Custom-Header", "value"); return response; } catch (err) { return new Response("Server Error", { status: 500 }); } }; // Chain multiple middlewares 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 auth(context) { const token = context.request.headers.get("Authorization"); if (!token) { return new Response("Unauthorized", { status: 401 }); } return context.next(); }
TypeScript Support
// functions/api/data.ts 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; // Access bindings 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
// functions/api/kv.ts interface Env { MY_KV: KVNamespace; } export const onRequest: PagesFunction<Env> = async ({ env }) => { // Write 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 }); // Read const value = await env.MY_KV.get("key"); const json = await env.MY_KV.get("json", { type: "json" }); // List const list = await env.MY_KV.list({ prefix: "user:" }); // Delete await env.MY_KV.delete("key"); return Response.json({ value, json }); };
# Local development npx wrangler pages dev ./dist --kv=MY_KV
R2 Bucket
// functions/api/files.ts 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 Response(object.body, { headers: { "Content-Type": object.httpMetadata?.contentType || "application/octet-stream" } }); case "DELETE": await env.BUCKET.delete(key); return new Response("Deleted"); } };
# Local development npx wrangler pages dev ./dist --r2=BUCKET
D1 Database
// functions/api/users.ts 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 }); };
# Local development npx wrangler pages dev ./dist --d1=DB=<database-id>
Durable Objects
// functions/api/counter.ts 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); };
# Local development (requires Worker with DO) npx wrangler pages dev ./dist --do=COUNTER=CounterDO@counter-worker
Service Bindings
// functions/api/proxy.ts interface Env { AUTH_SERVICE: Fetcher; } export const onRequest: PagesFunction<Env> = async ({ env, request }) => { // Call another Worker return env.AUTH_SERVICE.fetch(request); };
Queue Producer
// functions/api/jobs.ts 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
// functions/api/ai.ts 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
takes precedence overexcludeinclude- 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:
(configurable)main - Preview branches: All non-production (configurable)
- Custom rules: Include/exclude patterns
Direct Upload (Wrangler)
# Create project npx wrangler pages project create my-app # List projects npx wrangler pages project list # Deploy production npx wrangler pages deploy ./dist # Deploy preview npx wrangler pages deploy ./dist --branch=feature-x # List deployments npx wrangler pages deployment list --project-name=my-app
API Deployment
# Create deployment via API 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
# Basic development npx wrangler pages dev ./dist # With bindings npx wrangler pages dev ./dist \ --kv=MY_KV \ --r2=BUCKET \ --d1=DB=<database-id> \ --port=8788 # With live reload (framework dev server) npx wrangler pages dev -- npm run dev # Proxy to another server npx wrangler pages dev --proxy=3000
wrangler.toml Configuration
name = "my-pages-project" pages_build_output_dir = "./dist" # Compatibility compatibility_date = "2024-01-01" compatibility_flags = ["nodejs_compat"] # Environment variables [vars] API_URL = "https://api.example.com" # KV binding [[kv_namespaces]] binding = "MY_KV" id = "abc123" # R2 binding [[r2_buckets]] binding = "BUCKET" bucket_name = "my-bucket" # D1 binding [[d1_databases]] binding = "DB" database_name = "my-db" database_id = "xyz789" # Durable Objects [[durable_objects.bindings]] name = "COUNTER" class_name = "Counter" script_name = "counter-worker" # Service binding [[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
# List deployments npx wrangler pages deployment list --project-name=my-app # Rollback (redeploy a previous deployment) # Note: Direct rollback command not available; # redeploy the specific commit or use dashboard
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
isn't excluding the path_routes.json - Confirm file exists in build output
- Review
for conflicts_redirects
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
to skip Functions for static paths_routes.json - 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