_SH Log's
Back to Root
EST: 5 min read

Deploy Next.js to Cloudflare Pages: Full Guide

Deploying Next.js static exports to Cloudflare Pages gives you global CDN, zero cold starts, and free tier hosting. Here's the complete setup guide for 2026.

#cloudflare#nextjs#deployment#performance

Cloudflare Pages is the best hosting option for Next.js static exports in 2026: global CDN with 300+ PoPs, unlimited bandwidth on the free tier, automatic HTTPS, and zero cold starts. Here's the complete setup — including the gotchas that aren't in the official docs.

Why Cloudflare Pages for Next.js

| Feature | Cloudflare Pages | Vercel | Netlify | |---------|-----------------|--------|---------| | Free bandwidth | Unlimited | 100GB | 100GB | | Cold starts | ❌ None (static) | ❌ Edge Functions | ❌ Functions | | Global CDN | 300+ PoPs | ~70 regions | ~100 PoPs | | Custom domains | Free | Free | Free | | Build minutes | 500/month free | 6000/month | 300/month | | Static export support | ✅ Perfect | ✅ Perfect | ✅ Perfect |

For a static Next.js blog or landing page, Cloudflare Pages is strictly better than Vercel Free on bandwidth and CDN coverage.

Next.js config for static export

// next.config.ts
import type { NextConfig } from "next"

const config: NextConfig = {
  output: "export",
  trailingSlash: true,        // Required: CF Pages serves /slug/index.html
  images: { unoptimized: true } // Required: no Next.js image optimization in static
}

export default config

The three settings are all required. output: "export" generates static HTML in /out. trailingSlash: true ensures URLs match Cloudflare's file serving. images: { unoptimized: true } prevents Next.js from trying to optimize images at runtime (not possible in static export).

wrangler.toml configuration

# wrangler.toml
name = "blog-shihub"
compatibility_date = "2026-01-01"

[site]
bucket = "./out"     # where next build puts the static output

[[redirects]]
from = "/feed"
to = "/rss.xml"
status = 301

The [site] section tells Wrangler where your static files are. Redirects work natively in Cloudflare Pages — no separate redirect file needed.

GitHub Actions CI/CD

# .github/workflows/deploy.yml
name: Deploy to Cloudflare Pages

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      deployments: write

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: "22"
          cache: "npm"

      - run: npm ci
      - run: npm run build  # next build → /out

      - name: Deploy to Cloudflare Pages
        uses: cloudflare/pages-action@v1
        with:
          apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
          projectName: blog-shihub
          directory: out
          gitHubToken: ${{ secrets.GITHUB_TOKEN }}

The CLOUDFLARE_API_TOKEN needs Cloudflare Pages:Edit permission. Create it in Cloudflare Dashboard → My Profile → API Tokens.

Common gotchas

1. Trailing slash 404s

Without trailingSlash: true, /blog-post works but /blog-post/ returns 404 (Cloudflare serves index.html at the trailing slash path).

2. sitemap.ts and robots.ts need force-static

// app/sitemap.ts
export const dynamic = "force-static"  // Required for static export

export default function sitemap() { /* ... */ }

Without force-static, next build throws: "Page cannot be statically generated because it used headers."

3. CSS type declarations for strict TypeScript

If you import CSS files in TypeScript with strict mode:

// types/globals.d.ts
declare module "*.css" {
  const content: { [className: string]: string }
  export default content
}

Without this, tsc throws on import "./styles.css" in strict mode.

4. Dynamic routes need generateStaticParams

// app/[slug]/page.tsx
export async function generateStaticParams() {
  return getAllPosts().map(post => ({ slug: post.slug }))
}

export const dynamicParams = false  // 404 for slugs not in list

generateStaticParams is required for any dynamic route in static export mode.

IndexNow with Cloudflare Workers

Wire IndexNow to notify Bing/DuckDuckGo when new content deploys:

// workers/indexnow.ts (Cloudflare Worker)
export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const sitemapUrl = "https://blog.shihub.online/sitemap.xml"
    const sitemapResponse = await fetch(sitemapUrl)
    const sitemap = await sitemapResponse.text()

    // Extract URLs from sitemap
    const urls = [...sitemap.matchAll(/<loc>(.*?)<\/loc>/g)]
      .map(m => m[1])

    // Submit to IndexNow
    await fetch("https://api.indexnow.org/indexnow", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        host: "blog.shihub.online",
        key: env.INDEXNOW_KEY,
        urlList: urls,
      }),
    })

    return new Response("Submitted", { status: 200 })
  }
}

Trigger via Cloudflare Pages deployment webhook → Cloudflare Worker → IndexNow API.

Performance results

blog.shihub.online on Cloudflare Pages:

| Metric | Score | |--------|-------| | Lighthouse Performance | 99 | | FCP | 0.4s | | LCP | 0.6s | | TBT | 0ms | | CLS | 0 | | TTFB (global CDN) | < 30ms |

Sub-30ms TTFB globally is the main win vs managed Node.js hosting.

FAQ

Is Next.js static export limited compared to full Next.js on Vercel? Yes — you lose Server Actions, API Routes (runtime), ISR, and dynamic getServerSideProps. For blogs, landing pages, and documentation sites, static export covers everything needed.

Does Cloudflare Pages support Edge Functions with Next.js? Yes, via the @cloudflare/next-on-pages adapter. This enables API routes and server-side rendering on Cloudflare's edge network. Not covered here — this guide focuses on static export, which is simpler and covers most use cases.

How do I handle form submissions on a static Next.js site? Use a third-party form service (Formspree, Netlify Forms, or your own API on a separate domain). Static export means no server-side form processing.

How fast does Cloudflare Pages deploy? Typically 60–90 seconds for a Next.js static export. Build (next build) takes most of that time; Cloudflare's deploy step is ~10 seconds.


Written by Shihab Shahriar Antor — AI Engineer & Founder of Shahriar Labs. See also: Next.js 15 App Router: SEO & Performance Guide · SEO, GEO, and AEO for AI-Native Products in 2026.