|$ curl https://forge-ai.dev/api/markdown?path=docs/deploy/cloudflare
$cat docs/cloudflare-pages-—-edge-deployment.md
updated Recently·45 min read·published

Cloudflare Pages — Edge Deployment

EdgeServerlessBeginner to Advanced🎯Free Tools
Introduction

Cloudflare Pages deploys static sites and full-stack applications to Cloudflare's global edge network — 300+ data centers worldwide. Requests are served from the nearest edge location, providing sub-50ms latency for most users globally.

The free tier includes unlimited bandwidth, 500 builds per month, and access to Workers, D1, R2, and KV — making it one of the most generous hosting platforms available.

Getting Started

Connect your Git repository to Cloudflare Pages for automatic deployments on every push. Preview deployments are created for every pull request with unique URLs.

terminal
Bash
1# Install Wrangler CLI
2npm install -g wrangler
3
4# Login to Cloudflare
5wrangler login
6
7# Create a new Pages project
8wrangler pages project create my-app
9
10# Deploy a static directory
11wrangler pages deploy ./dist --project-name my-app
12
13# Deploy with a framework preset
14wrangler pages deploy . --project-name my-app \
15 --build-command "npm run build" \
16 --build-output-dir "./.next"
wrangler.toml
TOML
1name = "my-app"
2compatibility_date = "2024-01-15"
3
4[site]
5bucket = "./dist"
6
7[build]
8command = "npm run build"
9output = ".next"
10
11[vars]
12NODE_ENV = "production"
13
14[[env.production.kv_namespaces]]
15binding = "CACHE"
16id = "abc123..."

info

Use wrangler pages deploy instead of wrangler deploy for Pages projects. The Pages-specific command handles build configuration automatically.
Framework Support

Cloudflare Pages has built-in build presets for popular frameworks. The build system auto-detects your framework and configures the build command and output directory.

FrameworkBuild CommandOutput Dir
Next.jsnpx @cloudflare/next-on-pages.vercel/output/static
Astroastro builddist
Nuxtnuxt builddist
Remixremix vite:buildbuild/client
SvelteKitnpx sv build.svelte-kit/cloudflare
Vite (SPA)vite builddist
wrangler.toml
TOML
1# Next.js on Cloudflare Pages
2name = "my-next-app"
3compatibility_date = "2024-01-15"
4
5[build]
6command = "npx @cloudflare/next-on-pages"
7output = ".vercel/output/static"
8
9[vars]
10NEXT_PUBLIC_API_URL = "https://api.example.com"

warning

Next.js on Cloudflare Pages uses the @cloudflare/next-on-pages adapter. Not all Next.js features are supported — check the compatibility documentation for features like Image Optimization and Route Handlers.
Workers — Edge Functions

Workers run JavaScript, TypeScript, Python, or Rust at the edge with zero cold starts. They execute within 5ms of the user and can access Cloudflare's storage primitives directly.

src/index.ts
TypeScript
1// Worker serving API routes at the edge
2export default {
3 async fetch(request: Request, env: Env): Promise<Response> {
4 const url = new URL(request.url);
5
6 // Route to different handlers
7 if (url.pathname === "/api/hello") {
8 return Response.json({
9 message: "Hello from the edge!",
10 region: request.cf?.colo,
11 timestamp: new Date().toISOString(),
12 });
13 }
14
15 if (url.pathname === "/api/lookup") {
16 const ip = request.headers.get("cf-connecting-ip");
17 const country = request.cf?.country;
18
19 return Response.json({ ip, country });
20 }
21
22 if (url.pathname.startsWith("/api/users")) {
23 // Query D1 database at the edge
24 const userId = url.pathname.split("/").pop();
25 const user = await env.DB.prepare(
26 "SELECT * FROM users WHERE id = ?"
27 ).bind(userId).first();
28
29 if (!user) {
30 return Response.json({ error: "Not found" }, { status: 404 });
31 }
32
33 return Response.json(user);
34 }
35
36 return Response.json({ error: "Not found" }, { status: 404 });
37 },
38};
39
40interface Env {
41 DB: D1Database;
42 CACHE: KVNamespace;
43 BUCKET: R2Bucket;
44}
wrangler.toml
TOML
1# Worker bindings in wrangler.toml
2[[kv_namespaces]]
3binding = "CACHE"
4id = "kv-abc123"
5
6[[d1_databases]]
7binding = "DB"
8database_name = "my-database"
9database_id = "d1-abc123"
10
11[[r2_buckets]]
12binding = "BUCKET"
13bucket_name = "my-bucket"

best practice

Workers have a 10ms CPU time limit on the free plan (50ms on paid). For longer operations, use Durable Objects or queue tasks with Cloudflare Queues for async processing.
D1 — SQLite at the Edge

D1 is a serverless SQLite database that runs at the edge. It replicates read copies to every Cloudflare data center for sub-5ms read latency globally, while writes go to the primary in your configured region.

terminal
Bash
1# Create a D1 database
2wrangler d1 create my-database
3
4# Execute a migration
5wrangler d1 execute my-database --file=./schema.sql
6
7# Query from the command line
8wrangler d1 execute my-database --command "SELECT * FROM users LIMIT 5"
schema.sql
SQL
1CREATE TABLE users (
2 id INTEGER PRIMARY KEY AUTOINCREMENT,
3 email TEXT NOT NULL UNIQUE,
4 name TEXT NOT NULL,
5 created_at DATETIME DEFAULT CURRENT_TIMESTAMP
6);
7
8CREATE INDEX idx_users_email ON users(email);
9
10CREATE TABLE posts (
11 id INTEGER PRIMARY KEY AUTOINCREMENT,
12 user_id INTEGER NOT NULL,
13 title TEXT NOT NULL,
14 content TEXT,
15 published BOOLEAN DEFAULT FALSE,
16 created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
17 FOREIGN KEY (user_id) REFERENCES users(id)
18);
19
20CREATE INDEX idx_posts_user ON posts(user_id);
21CREATE INDEX idx_posts_published ON posts(published);
src/index.ts
TypeScript
1// D1 queries in a Worker
2export default {
3 async fetch(request: Request, env: Env): Promise<Response> {
4 const url = new URL(request.url);
5
6 // List users with pagination
7 if (url.pathname === "/api/users") {
8 const page = parseInt(url.searchParams.get("page") || "1");
9 const limit = parseInt(url.searchParams.get("limit") || "20");
10 const offset = (page - 1) * limit;
11
12 const { results, success } = await env.DB.prepare(
13 "SELECT id, email, name, created_at FROM users ORDER BY created_at DESC LIMIT ? OFFSET ?"
14 ).bind(limit, offset).all();
15
16 if (!success) {
17 return Response.json({ error: "Query failed" }, { status: 500 });
18 }
19
20 return Response.json({ users: results, page, limit });
21 }
22
23 // Create a user
24 if (url.pathname === "/api/users" && request.method === "POST") {
25 const { email, name } = await request.json();
26
27 try {
28 await env.DB.prepare(
29 "INSERT INTO users (email, name) VALUES (?, ?)"
30 ).bind(email, name).run();
31
32 return Response.json({ success: true }, { status: 201 });
33 } catch (e) {
34 if (e.message?.includes("UNIQUE")) {
35 return Response.json({ error: "Email already exists" }, { status: 409 });
36 }
37 throw e;
38 }
39 }
40
41 return Response.json({ error: "Not found" }, { status: 404 });
42 },
43};

info

D1 is ideal for read-heavy workloads. Use batch queries with DB.batch() to execute multiple statements in a single request, reducing round trips and staying within CPU limits.
R2 — Object Storage

R2 is S3-compatible object storage with zero egress fees. Store user uploads, static assets, backups, and any large files. Access directly from Workers at the edge.

src/index.ts
TypeScript
1// Upload file to R2
2export default {
3 async fetch(request: Request, env: Env): Promise<Response> {
4 const url = new URL(request.url);
5
6 // PUT /api/upload/:key
7 if (request.method === "PUT" && url.pathname.startsWith("/api/upload/")) {
8 const key = url.pathname.split("/api/upload/")[1];
9 const contentType = request.headers.get("content-type") || "application/octet-stream";
10
11 await env.BUCKET.put(key, request.body, {
12 httpMetadata: { contentType },
13 customMetadata: {
14 uploadedAt: new Date().toISOString(),
15 },
16 });
17
18 return Response.json({ key, contentType }, { status: 201 });
19 }
20
21 // GET /api/files/:key
22 if (url.pathname.startsWith("/api/files/")) {
23 const key = url.pathname.split("/api/files/")[1];
24 const object = await env.BUCKET.get(key);
25
26 if (!object) {
27 return Response.json({ error: "Not found" }, { status: 404 });
28 }
29
30 const headers = new Headers();
31 object.writeHttpMetadata(headers);
32 headers.set("etag", object.httpEtag);
33
34 return new Response(object.body, { headers });
35 }
36
37 // List files
38 if (url.pathname === "/api/files") {
39 const listed = await env.BUCKET.list({ limit: 100 });
40 return Response.json({
41 objects: listed.objects.map((o) => ({
42 key: o.key,
43 size: o.size,
44 uploaded: o.uploaded,
45 })),
46 });
47 }
48
49 return Response.json({ error: "Not found" }, { status: 404 });
50 },
51};

best practice

R2 has zero egress fees — a significant advantage over S3 for bandwidth-heavy applications. Use it for CDN assets, user uploads, and any data that gets frequently read.
KV — Key-Value Store

KV is an eventually-consistent, global key-value store optimized for read-heavy workloads. Use it for feature flags, session data, cached API responses, and configuration that changes infrequently.

src/index.ts
TypeScript
1// KV for caching and configuration
2export default {
3 async fetch(request: Request, env: Env): Promise<Response> {
4 const url = new URL(request.url);
5
6 // GET /api/config/:key
7 if (url.pathname.startsWith("/api/config/")) {
8 const key = url.pathname.split("/api/config/")[1];
9 const value = await env.CACHE.get(key, { type: "json" });
10
11 if (!value) {
12 return Response.json({ error: "Key not found" }, { status: 404 });
13 }
14
15 return Response.json({ key, value });
16 }
17
18 // PUT /api/config/:key
19 if (request.method === "PUT" && url.pathname.startsWith("/api/config/")) {
20 const key = url.pathname.split("/api/config/")[1];
21 const value = await request.json();
22
23 await env.CACHE.put(key, JSON.stringify(value), {
24 expirationTtl: 3600, // 1 hour
25 });
26
27 return Response.json({ success: true });
28 }
29
30 // Cached API response pattern
31 if (url.pathname === "/api/data") {
32 const cacheKey = "api:data:latest";
33 const cached = await env.CACHE.get(cacheKey, { type: "json" });
34
35 if (cached) {
36 return Response.json(cached);
37 }
38
39 // Fetch fresh data
40 const data = await fetchExternalApi();
41 await env.CACHE.put(cacheKey, JSON.stringify(data), {
42 expirationTtl: 300,
43 });
44
45 return Response.json(data);
46 }
47
48 return Response.json({ error: "Not found" }, { status: 404 });
49 },
50};

warning

KV is eventually consistent — writes may take up to 60 seconds to propagate globally. Don't use it for data that requires strong consistency. Use D1 for transactional data.
Custom Domains & Routing

Configure custom domains and routing rules to direct traffic to your Pages project. Cloudflare handles SSL certificates automatically.

wrangler.toml
TOML
1# wrangler.toml — custom routing
2name = "my-app"
3routes = [
4 { pattern = "example.com", zone_name = "example.com" },
5 { pattern = "example.com/api/*", zone_name = "example.com" },
6 { pattern = "docs.example.com/*", zone_name = "example.com" },
7]
src/index.ts
TypeScript
1// Worker with routing logic
2export default {
3 async fetch(request: Request, env: Env): Promise<Response> {
4 const url = new URL(request.url);
5
6 // Redirect www to apex
7 if (url.hostname.startsWith("www.")) {
8 url.hostname = url.hostname.replace("www.", "");
9 return Response.redirect(url.toString(), 301);
10 }
11
12 // API routes go to Worker
13 if (url.pathname.startsWith("/api/")) {
14 return handleApi(request, env);
15 }
16
17 // Everything else goes to Pages static assets
18 return env.ASSETS.fetch(request);
19 },
20};

info

Use env.ASSETS.fetch(request) to serve static assets from Pages within a Worker. This lets you combine dynamic Worker logic with static file serving in a single project.
Preview Deployments

Every pull request automatically gets a unique preview URL. This enables reviewing changes in a production-like environment before merging, preventing broken deployments.

.github/workflows/preview.yml
YAML
1# GitHub Actions — deploy preview on PR
2name: Preview Deploy
3on:
4 pull_request:
5 branches: [main]
6
7jobs:
8 deploy-preview:
9 runs-on: ubuntu-latest
10 steps:
11 - uses: actions/checkout@v4
12
13 - name: Deploy to Cloudflare Pages
14 uses: cloudflare/wrangler-action@v3
15 with:
16 apiToken: $\{{ secrets.CLOUDFLARE_API_TOKEN }}
17 accountId: $\{{ secrets.CLOUDFLARE_ACCOUNT_ID }}
18 command: pages deploy dist --branch=pr-$\{{ github.event.pull_request.number }}

Comment on the PR with the preview URL automatically:

.github/workflows/preview.yml
YAML
1 - name: Comment PR with preview URL
2 uses: actions/github-script@v7
3 with:
4 script: |
5 github.rest.issues.createComment({
6 issue_number: context.issue.number,
7 owner: context.repo.owner,
8 repo: context.repo.repo,
9 body: '🚀 Preview deployed: https://pr-$\{{ github.event.pull_request.number }}.my-app.pages.dev'
10 })
Performance Optimization

Cloudflare Pages provides built-in performance features. Understanding and configuring these correctly maximizes your application's speed.

Automatic Minification

Cloudflare automatically minifies HTML, CSS, and JavaScript at the edge. Enable Brotli compression in the dashboard for 15-20% smaller payloads compared to gzip.

Edge Caching

Set Cache-Control headers on your responses. Cloudflare respects these and caches at the edge. Use stale-while-revalidate for optimal freshness.

Image Optimization

Cloudflare Images and Image Resizing transform images at the edge. Resize, convert formats (WebP, AVIF), and strip metadata on the fly without impacting origin performance.

best practice

Enable Cloudflare's Auto Minify, Brotli compression, and HTTP/3 in the dashboard. These are free and significantly improve load times with zero code changes.

Blueprint — Engineering Documentation · Section ID: DEP-CFP-01 · Revision: 1.0