Steven's Knowledge

Getting Started

Deploy a Next.js site to Cloudflare Pages - one git push, custom domain, preview URLs, env vars

Getting Started

This page deploys a real frontend to Cloudflare Pages end-to-end. Patterns transfer to Vercel, Netlify — the dashboards differ but the moving parts are similar.

We'll use Next.js as the example, but Astro, SvelteKit, Nuxt, Remix all follow similar shapes.

Create a Project

# Bootstrap a Next.js app
npx create-next-app@latest my-site
cd my-site
git init
git add . && git commit -m "Initial commit"

# Push to GitHub
gh repo create my-site --public --source=. --remote=origin --push

Connect to Cloudflare Pages

  1. Sign in at dash.cloudflare.com.
  2. Workers & Pages → Create application → Pages → Connect to Git.
  3. Authorize GitHub; select your my-site repo.
  4. Choose the build settings:
Production branch: main
Framework preset: Next.js (auto-detected)
Build command: npm run build
Build output directory: .vercel/output/static    # for Next.js on Pages

For Next.js on Cloudflare Pages, install the adapter:

npm install --save-dev @cloudflare/next-on-pages

Update package.json:

{
  "scripts": {
    "build": "next-on-pages",
    "dev": "next dev"
  }
}
  1. Save and Deploy.

In ~30 seconds: your site is live at https://my-site-abc.pages.dev.

Push Updates

Now the magic:

echo "## Hello world" >> README.md
git commit -am "Update README"
git push

Within seconds, Pages detected the push, rebuilt, deployed. Your live URL serves the new version atomically.

Preview Deploys for Branches

git checkout -b feature/new-design
# make changes
git push -u origin feature/new-design

Pages builds the branch and posts a unique preview URL: https://feature-new-design.my-site-abc.pages.dev. Every PR has its own URL — share it with reviewers, designers, PMs.

When you merge to main, the preview URL is destroyed; production deploys.

Custom Domain

In the Pages project → Custom Domains → Set up a custom domain.

Add app.example.com. Cloudflare auto-creates a CNAME record (if your DNS is also Cloudflare; otherwise it gives you the value to add).

Within minutes, https://app.example.com serves your site with auto-provisioned HTTPS.

Environment Variables

Two scopes:

  • Production — used when building from main.
  • Preview — used for all preview branches.

In the Pages project → Settings → Environment variables → Add:

NEXT_PUBLIC_API_URL  =  https://api.example.com  (production)
NEXT_PUBLIC_API_URL  =  https://staging-api.example.com  (preview)
DATABASE_URL         =  postgres://...  (production, secret)

NEXT_PUBLIC_* variables are embedded into the client-side bundle at build time. Other variables are available to your server-side / edge functions at runtime.

For secrets, always mark as encrypted. Cloudflare encrypts at rest and only reveals at build / runtime.

Static vs Server Rendering

Modern frameworks split your pages between:

TypeWhere it runsWhen
Static (SSG)Pre-rendered HTML, served from edgePage content doesn't depend on the request
SSR (Server-Side Rendering)Server / edge function on each requestPer-user / per-request content
ISR (Incremental Static Regeneration)Pre-rendered, periodically rebuiltMostly static, occasional updates
Client-only (CSR)Browser fetches data after loadHighly dynamic, less SEO-critical

For Next.js, opt in per page:

// app/products/page.tsx — static (default in App Router)
export default async function ProductsPage() {
  const products = await fetch('https://api.example.com/products').then(r => r.json());
  return <ProductList products={products} />;
}

// app/products/page.tsx — revalidate every 60s (ISR)
export const revalidate = 60;

// app/dashboard/page.tsx — force dynamic / SSR
export const dynamic = 'force-dynamic';

Modern static hosts auto-detect this and deploy each page to the right tier — static assets to CDN, dynamic pages to edge functions.

Edge Functions in Static Sites

A Pages project can ship Cloudflare Workers alongside the static assets:

my-site/
├── app/                  ← Next.js pages
├── public/               ← static assets
└── functions/            ← Cloudflare Pages Functions
    └── api/
        └── hello.ts
// functions/api/hello.ts
export const onRequest: PagesFunction = async (context) => {
  return new Response(JSON.stringify({ msg: 'hi from edge' }), {
    headers: { 'content-type': 'application/json' },
  });
};

Hit /api/hello on your deployed site → runs at the edge, near every user. Same shape as standalone Edge Functions.

Equivalent: Vercel

npm install --global vercel
vercel                  # interactive setup
vercel --prod           # deploy to production

Or via git: connect repo on vercel.com, deploys on push. Vercel auto-detects Next.js (created by them) — usually zero config.

Equivalent: Netlify

npm install --global netlify-cli
netlify init           # connect to a Netlify site
netlify deploy --prod

Or git-based via the Netlify UI. Choose your framework; Netlify configures the build.

Build Cache

First builds are slow (npm install, etc.). Subsequent builds use cache:

  • Vercel — caches node_modules, .next/cache, etc. automatically.
  • Cloudflare Pages — caches node_modules automatically; configurable.
  • Netlify — caches node_modules and configurable paths.

Build times drop from 2-3 minutes to 30-60 seconds for typical Next.js sites after the first build.

Rollback

Every deploy is preserved. Click "Promote to production" on any previous deploy in the dashboard to roll back instantly.

# Vercel CLI
vercel rollback

# Cloudflare Pages — UI: pick a previous deployment, "Promote to Production"

Rollback is faster than deploying a fix-forward — when production breaks, roll back, then debug.

Tear Down

Remove the connection at the platform dashboard (or vercel remove). Your repo stays; only the deployment goes.

What's Next

You've deployed a frontend with preview URLs, env vars, and a custom domain. Next:

  • Patterns — static vs SSR vs ISR, preview environments, edge functions, image optimization
  • Best Practices — build optimization, performance, observability, cost

On this page