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 --pushConnect to Cloudflare Pages
- Sign in at
dash.cloudflare.com. - Workers & Pages → Create application → Pages → Connect to Git.
- Authorize GitHub; select your
my-siterepo. - 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 PagesFor Next.js on Cloudflare Pages, install the adapter:
npm install --save-dev @cloudflare/next-on-pagesUpdate package.json:
{
"scripts": {
"build": "next-on-pages",
"dev": "next dev"
}
}- 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 pushWithin 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-designPages 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:
| Type | Where it runs | When |
|---|---|---|
| Static (SSG) | Pre-rendered HTML, served from edge | Page content doesn't depend on the request |
| SSR (Server-Side Rendering) | Server / edge function on each request | Per-user / per-request content |
| ISR (Incremental Static Regeneration) | Pre-rendered, periodically rebuilt | Mostly static, occasional updates |
| Client-only (CSR) | Browser fetches data after load | Highly 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 productionOr 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 --prodOr 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_modulesautomatically; configurable. - Netlify — caches
node_modulesand 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