Reference

Cloudflare Cheatsheet

Production cheatsheet from ActiveWizards deployment (CF Pages + Functions + DNS).


Wrangler CLI

# Deploy a static site (pre-built)
npx wrangler pages deploy dist --project-name=my-site --branch=main

# Deploy with dirty git state (CI/local)
npx wrangler pages deploy dist --project-name=my-site --commit-dirty=true --branch=main

# Set secrets (not visible in dashboard)
npx wrangler pages secret put MAILGUN_API_KEY

# List projects
npx wrangler pages list

# Tail logs (Functions)
npx wrangler pages deployment tail --project-name=my-site

Deploy Script Pattern (multi-site)

#!/usr/bin/env bash
set -euo pipefail
SITE_DIR="$(cd "$(dirname "$0")" && pwd)/site"

deploy_astro() {
    local site="$1" project="$2" branch="${3:-staging}"
    (cd "${SITE_DIR}/${site}" && npm run build)
    npx wrangler pages deploy "${SITE_DIR}/${site}/dist" \
      --project-name="${project}" --commit-dirty=true --branch="${branch}"
}

deploy_static() {
    local site="$1" project="$2"
    npx wrangler pages deploy "${SITE_DIR}/${site}" \
      --project-name="${project}" --commit-dirty=true --branch=main
}

case "${1:-}" in
    staging)  deploy_astro my-site my-project staging ;;
    prod)     deploy_astro my-site my-project main ;;
    *)        echo "Usage: $0 [staging|prod]"; exit 1 ;;
esac

_redirects File

Place in public/_redirects (copied to dist/ on build).

Format

<from> <to> <status>

Examples

# Simple redirect

/old-url /new-url 301

# With trailing slash normalization

/contact/ /contact-us/ 301

# Splat (wildcard)

/old-blog/* /blog/:splat 301

# SPA fallback (DON'T use for SSG sites)

/* /index.html 200

Rules

  • Max 2000 rules (hard limit)
  • Processed top to bottom, first match wins
  • Splats: * captures everything, :splat inserts it in destination
  • Placeholders: /blog/:slug matches /blog/anything
  • Static files take priority over redirects (a redirect won't override an actual file)
  • Duplicate rules are silently ignored (no error, first one wins)

Production note

AW site has 804+ redirect rules for MODx migration. Bulk-generated by scripts/migration_decision.py.


Pages Functions

Files in functions/ directory map to API routes:

File pathRoute

functions/api/contact.tsPOST /api/contact
functions/api/[id].tsGET /api/:id
functions/_middleware.tsRuns on all routes

Basic Function

interface Env {
  MAILGUN_API_KEY: string;
  MAILGUN_DOMAIN: string;
  NOTIFICATION_EMAIL: string;
}

export const onRequestPost: PagesFunction<Env> = async (context) => {
  const { request, env } = context;
  const formData = await request.formData();

  const name = formData.get("name")?.toString().trim() || "";
  const email = formData.get("email")?.toString().trim() || "";

  if (!name || !email) {
    return new Response(JSON.stringify({ error: "Missing fields" }), {
      status: 400,
      headers: { "Content-Type": "application/json" },
    });
  }

  // Fire-and-forget with waitUntil (response returns immediately)
  context.waitUntil(
    fetch(https://api.mailgun.net/v3/${env.MAILGUN_DOMAIN}/messages, {
      method: "POST",
      headers: { Authorization: "Basic " + btoa(api:${env.MAILGUN_API_KEY}) },
      body: buildFormData(name, email, env),
    }).catch((err) => console.error("Mailgun failed:", err))
  );

  return new Response(JSON.stringify({ success: true }), {
    status: 200,
    headers: { "Content-Type": "application/json" },
  });
};

Key patterns

  • context.waitUntil(): Fire-and-forget async work. Response returns immediately while the email sends in the background.
  • onRequestPost / onRequestGet: Method-specific handlers.
  • onRequest: Catches all methods.
  • Env typing: PagesFunction gives typed access to context.env.

Dual response (JSON + redirect)

function jsonOrRedirect(request: Request, data: object, redirectUrl: string): Response {
  const accept = request.headers.get("Accept") || "";
  if (accept.includes("application/json")) {
    return new Response(JSON.stringify(data), {
      status: 200,
      headers: { "Content-Type": "application/json" },
    });
  }
  return Response.redirect(new URL(redirectUrl, request.url).toString(), 302);
}

This handles both fetch() (returns JSON) and

submissions (redirects).


Custom Domains

Subdomain (e.g., staging.example.com)

  • CF Dashboard -> Pages -> project -> Custom domains -> Add domain
  • Add CNAME record: staging -> project-name.pages.dev (proxied)
  • Root domain (e.g., example.com)

  • Domain must be on Cloudflare DNS
  • Add custom domain in Pages dashboard
  • CF auto-creates the DNS record
  • Preview deployments

    Every non-main branch gets a URL: ..pages.dev

    Deploy to staging branch:

    npx wrangler pages deploy dist --project-name=my-site --branch=staging

    Staging URL: staging..pages.dev


    Environment Variables & Secrets

    Dashboard

    Settings -> Environment variables -> Add variable

    • Can set different values for Production vs Preview
    • Visible in dashboard (not suitable for API keys)

    CLI (for secrets)

    npx wrangler pages secret put MAILGUN_API_KEY
    # Prompts for value (not stored in shell history)

    In Functions

    Access via context.env.VARIABLE_NAME (typed with PagesFunction).

    In Astro (build-time)

    CF Pages build environment variables are available as process.env.VARIABLE during build.

    For client-side: use PUBLIC_ prefix and import.meta.env.PUBLIC_*.


    Headers (public/_headers)

    /*
    

    X-Frame-Options: DENY

    X-Content-Type-Options: nosniff

    Referrer-Policy: strict-origin-when-cross-origin

    /fonts/*

    Cache-Control: public, max-age=31536000, immutable

    /*.css

    Cache-Control: public, max-age=31536000, immutable


    KV Bindings (for Functions)

    In wrangler.toml:

    [[kv_namespaces]]
    binding = "CACHE"
    id = "abc123"

    Access in function: context.env.CACHE.get("key") / .put("key", "value")


    Cache & Purge

    • Static assets: automatic CDN caching
    • Purge all: Dashboard -> Caching -> Configuration -> Purge Everything
    • Purge by URL: API or Dashboard
    • Pages Functions: not cached by default (dynamic)


    Build Configuration (Dashboard)

    SettingValue

    Framework presetAstro
    Build commandnpm run build
    Build output directorydist
    Root directory/ (or subfolder if monorepo)
    Node.js versionSet NODE_VERSION=20 in env vars


    Cloudflare + External Services

    Behind Cloudflare (proxied)

    If origin server should only accept CF traffic:

    # UFW rules on origin server — allow only Cloudflare IPs on 80/443
    

    # See https://www.cloudflare.com/ips/ for current list

    Firewall Rules

    • Dashboard -> Security -> WAF -> Custom rules
    • Rate limiting: Security -> WAF -> Rate limiting rules
    • Bot protection: free tier includes basic bot detection


    Common Gotchas

  • _redirects in public/: Must be in public/ for Astro (gets copied to dist/). Putting it in dist/ directly gets overwritten on build.
  • Functions + static: If a function path conflicts with a static file, the static file wins. Name your function routes to avoid collision (e.g., /api/*).
  • Secret rotation: wrangler pages secret put overwrites immediately. No restart needed — Functions pick up new values on next invocation.
  • Preview vs Production env vars: Set different values per environment in dashboard. Secrets set via CLI apply to both.
  • Max 100 redirects per request chain: CF will stop following after 100 hops (unlikely but worth knowing for bulk redirect files).
  • commit-dirty=true: Required flag when deploying from a repo with uncommitted changes. Without it, wrangler may refuse to deploy.
  • Branch-based environments: --branch=main = production. Any other branch name = preview. This is how staging/production separation works.
  • No server-side rendering by default: CF Pages Functions are for API routes. Astro SSR requires @astrojs/cloudflare adapter (not needed for SSG sites).
  • Large sites: Build output > 20,000 files may hit limits. Use astro-compress to reduce file count where possible.
  • CORS on Functions: CF Pages Functions don't add CORS headers automatically. Add them manually if calling from a different domain.