Complete Cloudflare Pages Complete Guide | Free Deployment, Edge Rendering, Wrangler CLI
이 글의 핵심
Covers deploying static sites and SSR apps for free with Cloudflare Pages, and running server logic with Edge Functions. From GitHub integration, Wrangler CLI, environment variables, custom domains to build optimization with practical examples.
Introduction
Cloudflare Pages is a platform for deploying static sites and Server Side Rendering (SSR) apps to a global Edge network. Similar to Vercel/Netlify, but strengths include unlimited free bandwidth, 300+ city CDN, and integration with Workers, D1, R2. This article covers GitHub repository integration, Wrangler CLI deployment, environment variables, custom domains, Functions (Edge server logic), build optimization, and comparison with Vercel through practical examples. For overall Node.js app deployment, see Node.js Deployment Guide (PM2, Docker, AWS, Nginx), and for CI/CD pipeline, see Node.js + GitHub Actions CI/CD.
Reality in Practice
When learning development, everything seems clean and theoretical. But practice is different. You wrestle with legacy code, chase tight deadlines, and face unexpected bugs. The content covered in this article was initially learned as theory, but it was through applying it to actual projects that I realized “Ah, this is why it’s designed this way.” What stands out in my memory is the trial and error from my first project. I did everything by the book but couldn’t figure out why it wasn’t working, spending days struggling. Eventually, through a senior developer’s code review, I discovered the problem and learned a lot in the process. In this article, I’ll cover not just theory but also the pitfalls you might encounter in practice and how to solve them.
Table of Contents
- What is Cloudflare Pages?
- GitHub Integration Deployment
- Wrangler CLI Deployment
- Environment Variables and Secrets
- Custom Domain Setup
- Functions (Edge Server Logic)
- Build Optimization
- Vercel/Netlify Comparison
- Summary
1. What is Cloudflare Pages?
Core Features
| Item | Description |
|---|---|
| CDN | 300+ cities worldwide, Cloudflare network |
| Free Plan | 500 builds/month, unlimited requests/bandwidth |
| Supported Frameworks | React, Vue, Astro, Next.js, SvelteKit, Remix, etc. |
| SSR/Edge | Cloudflare Workers-based Functions |
| Build Environment | Node.js, Python, Ruby, Go, etc. |
When to Use
- Static Sites: Blogs, documentation, landing pages
- JAMstack: Astro, Hugo, Jekyll, Eleventy
- SSR Apps: Next.js App Router, SvelteKit, Remix
- Edge API: Cloudflare Workers + D1(SQLite) + R2(S3 compatible) If traffic is global, bandwidth cost is a concern, or you want to run server logic on Edge, Cloudflare Pages is a strong choice.
2. GitHub Integration Deployment
The simplest method is connecting GitHub repository from Cloudflare dashboard.
2-1. Basic Setup
- Cloudflare Dashboard → Pages → Create a project
- Connect to Git → Connect GitHub account
- Select repository → Branch (
mainorproduction) - Build settings:
- Framework preset: Auto-detect Astro, Next.js, React, etc.
- Build command:
npm run build - Build output directory:
dist(Astro),out(Next.js),.next(Next.js SSR)
- Save and Deploy
2-2. Automatic Deployment
- Push to
mainbranch → Automatic build and deploy - Preview deployment created for each PR (URL:
<branch>.<project>.pages.dev) - Build logs viewable in real-time from dashboard
Pros: Easy setup, automatic PR preview.
Cons: Consumes Cloudflare build environment (500 free/month), limited build time/cache control.
3. Wrangler CLI Deployment
Wrangler is Cloudflare’s official CLI, allowing local build → upload to save Cloudflare build count.
3-1. Install Wrangler
npm install -g wrangler
# Or project local
npm install --save-dev wrangler
3-2. Authentication
wrangler login
Authenticate Cloudflare account in browser.
3-3. Deploy
Here’s an implementation example using bash. Try running the code directly to see how it works.
# Build locally
npm run build
# Upload dist folder to Cloudflare Pages
wrangler pages deploy dist --project-name=my-project
On first deployment, project is automatically created if it doesn’t exist.
3-4. Using Wrangler in GitHub Actions
Here’s a detailed implementation using YAML. Please review the code to understand the role of each part.
name: Deploy to Cloudflare Pages
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
env:
NODE_OPTIONS: '--max-old-space-size=4096'
- name: Deploy to Cloudflare Pages
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: pages deploy dist --project-name=my-project
Required secrets:
CLOUDFLARE_API_TOKEN: Dashboard → API Tokens → Edit Cloudflare Workers permissionCLOUDFLARE_ACCOUNT_ID: Check in dashboard URL (dash.cloudflare.com/<ACCOUNT_ID>/...) Pros: Full build environment control, free cache strategy, saves Cloudflare build count.
4. Environment Variables and Secrets
4-1. Set in Cloudflare Dashboard
Pages project → Settings → Environment variables
- Production: Used for
mainbranch deployment - Preview: Used for PR/branch deployment
DATABASE_URL=postgresql://...
API_KEY=abc123
Accessible during both build time (npm run build) and runtime (Functions).
4-2. Access in Code
Build Time (Node.js):
// astro.config.mjs, next.config.js, etc.
const apiKey = process.env.API_KEY;
Runtime (Cloudflare Functions): Here’s an implementation example using JavaScript. Perform tasks efficiently with async processing. Try running the code directly to see how it works.
// functions/api/data.js
export async function onRequest(context) {
const apiKey = context.env.API_KEY;
return new Response(JSON.stringify({ key: apiKey }));
}
4-3. Local Development
.dev.vars file (add to .gitignore):
DATABASE_URL=postgresql://localhost/dev
API_KEY=dev-key-123
wrangler pages dev dist --local
5. Custom Domain Setup
5-1. Add Domain
Pages project → Custom domains → Set up a custom domain
- Enter domain (e.g.,
blog.example.com) - Add DNS record:
- CNAME:
blog→<project>.pages.dev - Or A/AAAA: IP provided by Cloudflare
- CNAME:
- Automatic SSL issuance (Let’s Encrypt, free)
5-2. Apex Domain (example.com)
If managing domain with Cloudflare:
- CNAME flattening automatically applied
- Add
example.com→<project>.pages.devCNAME For other DNS providers, need to add A record with Cloudflare IP.
6. Functions (Edge Server Logic)
Cloudflare Pages Functions are Cloudflare Workers based, running server code on Edge.
6-1. Basic Structure
Here’s an implementation example using text. Try running the code directly to see how it works.
my-project/
├── functions/
│ ├── api/
│ │ ├── hello.js # /api/hello
│ │ └── users/[id].js # /api/users/:id
│ └── _middleware.js # Applied to all requests
├── public/
└── dist/
6-2. Example: API Endpoint
Here’s an implementation example using JavaScript. Perform tasks efficiently with async processing. Try running the code directly to see how it works.
// functions/api/hello.js
export async function onRequest(context) {
const { request, env, params } = context;
return new Response(
JSON.stringify({ message: 'Hello from Edge!' }),
{ headers: { 'Content-Type': 'application/json' } }
);
}
After deployment, access at https://my-project.pages.dev/api/hello.
6-3. Dynamic Routes
Here’s an implementation example using JavaScript. Perform tasks efficiently with async processing. Please review the code to understand the role of each part.
// functions/api/users/[id].js
export async function onRequest(context) {
const userId = context.params.id;
// D1 (Cloudflare SQLite) example
const db = context.env.DB;
const user = await db.prepare('SELECT * FROM users WHERE id = ?')
.bind(userId)
.first();
return new Response(JSON.stringify(user), {
headers: { 'Content-Type': 'application/json' }
});
}
6-4. Middleware
Here’s an implementation example using JavaScript. Perform tasks efficiently with async processing. Please review the code to understand the role of each part.
// functions/_middleware.js
export async function onRequest(context) {
const start = Date.now();
// Execute next handler
const response = await context.next();
// Add response header
response.headers.set('X-Response-Time', `${Date.now() - start}ms`);
return response;
}
6-5. SSR Frameworks
Astro SSR: Here’s an implementation example using JavaScript. Import necessary modules. Try running the code directly to see how it works.
// astro.config.mjs
import { defineConfig } from 'astro/config';
import cloudflare from '@astrojs/cloudflare';
export default defineConfig({
output: 'server', // or 'hybrid'
adapter: cloudflare()
});
Next.js:
npm install @cloudflare/next-on-pages
Here’s an implementation example using JavaScript. Try running the code directly to see how it works.
// next.config.js
module.exports = {
experimental: {
runtime: 'experimental-edge'
}
};
7. Build Optimization
7-1. GitHub Actions Build Cache
Here’s an implementation example using YAML. Please review the code to understand the role of each part.
- name: Cache dependencies
uses: actions/cache@v4
with:
path: |
node_modules
.astro
.next/cache
key: ${{ runner.os }}-build-${{ hashFiles('package-lock.json') }}
restore-keys: |
${{ runner.os }}-build-
7-2. Reduce Build Time
Parallel Processing: Here’s an implementation example using JavaScript. Import necessary modules, perform tasks efficiently with async processing, process data with loops. Please review the code to understand the role of each part.
// scripts/build.mjs
import { execSync } from 'child_process';
const tasks = [
'node scripts/generate-og-images.mjs',
'node scripts/generate-sitemap.mjs',
'node scripts/generate-rss.mjs'
];
await Promise.all(tasks.map(cmd =>
execSync(cmd, { stdio: 'inherit' })
));
execSync('astro build', { stdio: 'inherit' });
Incremental Build:
- Astro: Cache
.astrofolder - Next.js: Cache
.next/cachefolder
7-3. Large Page Optimization
Problem: 1,000+ pages → 10+ minute build Solution:
- OG Image Cache: Regenerate only changed posts
- Static Pages First:
output: 'static'orhybrid - Parallel Rendering: Astro 4.0+ automatic parallelization Here’s an implementation example using JavaScript. Try running the code directly to see how it works.
// astro.config.mjs
export default defineConfig({
output: 'static',
build: {
concurrency: 8 // Parallel rendering
}
});
8. Vercel/Netlify Comparison
8-1. Feature Comparison Table
| Item | Cloudflare Pages | Vercel | Netlify |
|---|---|---|---|
| Free Builds | 500/month | 6,000 min/month | 300 min/month |
| Free Bandwidth | Unlimited | 100GB/month | 100GB/month |
| CDN Locations | 300+ cities | 100+ cities | 100+ cities |
| Edge Functions | Workers (V8) | Edge Functions (V8) | Edge Functions (Deno) |
| DB Integration | D1 (SQLite), R2 (S3) | Vercel Postgres, Blob | Netlify Blobs |
| Build Time | Medium | Fast (Next.js optimized) | Medium |
| DX | Good | Very Good | Good |
8-2. Selection Criteria
Choose Cloudflare Pages when:
- High global traffic and bandwidth cost is a concern
- Using Cloudflare ecosystem like Workers, D1, R2
- Need unlimited bandwidth on free plan
- Building blog with static generators like Astro/Hugo (Astro Blog Guide reference) Choose Vercel when:
- Using Next.js App Router and prioritizing developer experience
- Build time is important and many preview deployments
- Using Vercel Analytics/Speed Insights Choose Netlify when:
- Need Form processing, Identity(auth), Split Testing
- Prefer Deno-based Edge Functions
9. Real Example: Astro Blog Deployment
9-1. Project Structure
Here’s an implementation example using text. Please review the code to understand the role of each part.
my-blog/
├── src/
│ ├── pages/
│ ├── content/
│ └── components/
├── public/
├── functions/
│ └── api/
│ └── views.js # View count API
├── astro.config.mjs
├── wrangler.toml
└── package.json
9-2. GitHub Actions Workflow
Here’s a detailed implementation using YAML. Please review the code to understand the role of each part.
name: Deploy to Cloudflare Pages
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Validate frontmatter
run: npm run validate
- name: Build
run: npm run build
env:
NODE_OPTIONS: '--max-old-space-size=4096'
- name: Deploy to Cloudflare Pages
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: pages deploy dist --project-name=my-blog --commit-message="Deploy from GitHub Actions"
9-3. View Count API (Functions)
Here’s a detailed implementation using JavaScript. Perform tasks efficiently with async processing, perform branching with conditionals. Please review the code to understand the role of each part.
// functions/api/views.js
export async function onRequest(context) {
const { request, env } = context;
const url = new URL(request.url);
const slug = url.searchParams.get('slug');
if (!slug) {
return new Response('Missing slug', { status: 400 });
}
// D1 (Cloudflare SQLite)
const db = env.DB;
if (request.method === 'POST') {
// Increment view count
await db.prepare('INSERT INTO views (slug, count) VALUES (?, 1) ON CONFLICT(slug) DO UPDATE SET count = count + 1')
.bind(slug)
.run();
}
// Query view count
const result = await db.prepare('SELECT count FROM views WHERE slug = ?')
.bind(slug)
.first();
return new Response(
JSON.stringify({ slug, views: result?.count || 0 }),
{ headers: { 'Content-Type': 'application/json' } }
);
}
D1 Binding (wrangler.toml):
Here’s an implementation example using TOML. Try running the code directly to see how it works.
name = "my-blog"
pages_build_output_dir = "dist"
[[d1_databases]]
binding = "DB"
database_name = "my-blog-db"
database_id = "abc123..."
10. Advanced Tips
10-1. Redirects
_redirects file (include in build output folder):
/old-url /new-url 301
/blog/* /posts/:splat 302
Or _headers:
/*
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
10-2. Limit Preview Branches
Here’s an implementation example using YAML. Try running the code directly to see how it works.
# .github/workflows/deploy-cloudflare.yml
on:
push:
branches: [main]
# Leave PR preview to Cloudflare automatic deployment
10-3. Build Failure Notification
Here’s an implementation example using YAML. Try running the code directly to see how it works.
- name: Notify on failure
if: failure()
run: |
curl -X POST ${{ secrets.SLACK_WEBHOOK }} \
-H 'Content-Type: application/json' \
-d '{"text":"Cloudflare Pages deployment failed!"}'
10-4. Rollback
Dashboard:
- Deployments → Select previous deployment → Rollback to this deployment CLI:
wrangler pages deployment list --project-name=my-blog
wrangler pages deployment rollback <deployment-id>
11. Summary
Key Summary
Cloudflare Pages Advantages:
- Unlimited free bandwidth
- 300+ city global CDN
- Workers, D1, R2 integration
- SSR/Edge Functions support Deployment Methods:
- GitHub Integration: Easiest, automatic preview
- Wrangler CLI: Build control, save count Recommended Configuration:
- Local/CI: Build in GitHub Actions + Wrangler upload
- Preview: Cloudflare automatic deployment
- Environment Variables: Separate Production/Preview in dashboard
- Monitoring: Cloudflare Analytics + Sentry
Checklist
Before Deployment:
- Check build command (
npm run build) - Check output directory (
dist,out,.next) - Set environment variables (API keys, DB URL)
- Add
.env,.dev.varsto.gitignoreAfter Deployment: - Check custom domain DNS propagation (up to 24 hours)
- Check SSL certificate issuance
- Test Functions operation
- Check 404 page
Next Steps
Good articles to use with Cloudflare Pages:
- Astro + Cloudflare Pages Stack Analysis — Compare with Vercel, Netlify, WordPress
- Building Tech Blog with Astro
- Node.js Deployment Guide (PM2, Docker, AWS)
- Node.js + GitHub Actions CI/CD
- Increasing Tech Blog Visitors — Structure search traffic after deployment References:
- Cloudflare Pages Official Documentation
- Wrangler CLI Documentation
- Cloudflare Workers Documentation