Complete Cloudflare Pages Complete Guide | Free Deployment, Edge Rendering, Wrangler CLI

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

  1. What is Cloudflare Pages?
  2. GitHub Integration Deployment
  3. Wrangler CLI Deployment
  4. Environment Variables and Secrets
  5. Custom Domain Setup
  6. Functions (Edge Server Logic)
  7. Build Optimization
  8. Vercel/Netlify Comparison
  9. Summary

1. What is Cloudflare Pages?

Core Features

ItemDescription
CDN300+ cities worldwide, Cloudflare network
Free Plan500 builds/month, unlimited requests/bandwidth
Supported FrameworksReact, Vue, Astro, Next.js, SvelteKit, Remix, etc.
SSR/EdgeCloudflare Workers-based Functions
Build EnvironmentNode.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

  1. Cloudflare DashboardPagesCreate a project
  2. Connect to Git → Connect GitHub account
  3. Select repository → Branch (main or production)
  4. 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)
  5. Save and Deploy

2-2. Automatic Deployment

  • Push to main branch → 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 TokensEdit Cloudflare Workers permission
  • CLOUDFLARE_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 projectSettingsEnvironment variables

  • Production: Used for main branch 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 projectCustom domainsSet up a custom domain

  1. Enter domain (e.g., blog.example.com)
  2. Add DNS record:
    • CNAME: blog<project>.pages.dev
    • Or A/AAAA: IP provided by Cloudflare
  3. 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.dev CNAME 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 .astro folder
  • Next.js: Cache .next/cache folder

7-3. Large Page Optimization

Problem: 1,000+ pages → 10+ minute build Solution:

  1. OG Image Cache: Regenerate only changed posts
  2. Static Pages First: output: 'static' or hybrid
  3. 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

ItemCloudflare PagesVercelNetlify
Free Builds500/month6,000 min/month300 min/month
Free BandwidthUnlimited100GB/month100GB/month
CDN Locations300+ cities100+ cities100+ cities
Edge FunctionsWorkers (V8)Edge Functions (V8)Edge Functions (Deno)
DB IntegrationD1 (SQLite), R2 (S3)Vercel Postgres, BlobNetlify Blobs
Build TimeMediumFast (Next.js optimized)Medium
DXGoodVery GoodGood

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:
  1. GitHub Integration: Easiest, automatic preview
  2. 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.vars to .gitignore After 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: