Skip to main content

Overview

Cloudflare Pages provides a fast, secure platform for deploying static sites and full-stack applications. For Fumadocs, you have two deployment options:
  1. Static Export - Build and deploy as static HTML/CSS/JS files
  2. Full-Stack with OpenNext - Deploy Next.js with server-side features
The simplest approach for most documentation sites.
1
Configure Static Export
2
Enable static export in your Next.js configuration:
3
import { createMDX } from 'fumadocs-mdx/next';

const withMDX = createMDX();

/** @type {import('next').NextConfig} */
const config = {
  output: 'export',
  reactStrictMode: true,
  images: {
    unoptimized: true,
  },
};

export default withMDX(config);
4
See Static Export for complete configuration details.
6
Set up client-side search for static sites:
7
import { SearchDialog } from 'fumadocs-ui/components/dialog/search';

export function Search() {
  return (
    <SearchDialog
      search={{
        type: 'static',
        index: '/static.json',
      }}
    />
  );
}
8
Build Your Site
9
npm run build
10
Static files are output to the out/ directory.
11
Deploy to Cloudflare Pages
12
Option 1: Wrangler CLI
13
# Install Wrangler
npm install -g wrangler

# Deploy
wrangler pages deploy out/
14
Option 2: Direct Upload
15
  • Go to Cloudflare Dashboard
  • Navigate to Workers & Pages → Create
  • Select “Upload assets”
  • Upload the out/ directory
  • 16
    Option 3: Git Integration
    17
    Connect your repository for automatic deployments:
    18
  • Workers & Pages → Create → Connect to Git
  • Select your repository
  • Configure build settings:
    • Build command: npm run build
    • Build output directory: out
  • Deploy
  • Full-Stack with OpenNext

    For Next.js applications requiring server-side features (API routes, SSR, ISR).
    Fumadocs does not work on Cloudflare’s Edge runtime. Use OpenNext for Cloudflare to deploy with server-side features.
    1
    Install OpenNext
    2
    npm install --save-dev @opennextjs/cloudflare
    
    3
    Configure OpenNext
    4
    Create an OpenNext configuration:
    5
    module.exports = {
      platform: 'cloudflare',
      cloudflare: {
        routes: [
          {
            pattern: '/api/*',
            custom_handler: true,
          },
        ],
      },
    };
    
    6
    Update Build Script
    7
    {
      "scripts": {
        "build": "next build && opennext build",
        "deploy": "wrangler pages deploy .open-next/worker"
      }
    }
    
    8
    Build and Deploy
    9
    npm run build
    npm run deploy
    

    Configuration

    wrangler.toml

    Configure Cloudflare Workers/Pages settings:
    wrangler.toml
    name = "fumadocs"
    pages_build_output_dir = "out"
    compatibility_date = "2024-01-01"
    compatibility_flags = ["nodejs_compat"]
    
    [env.production]
    route = "https://docs.example.com/*"
    
    [env.production.vars]
    NODE_ENV = "production"
    

    Environment Variables

    Set environment variables in Cloudflare Dashboard:
    1. Workers & Pages → Your Project → Settings
    2. Environment Variables → Add Variable
    3. Add for Production and Preview environments
    For local development:
    .dev.vars
    NEXT_PUBLIC_SITE_URL=http://localhost:3000
    API_KEY=your-secret-key
    
    .dev.vars is gitignored by default. Use it for local secrets.

    Custom Domain

    Add a custom domain:
    1
    Add Domain
    2
  • Workers & Pages → Your Project → Custom Domains
  • Click “Set up a custom domain”
  • Enter your domain (e.g., docs.example.com)
  • 3
    Configure DNS
    4
    If your domain is on Cloudflare:
    5
  • DNS records are automatically configured
  • 6
    If using external DNS:
    7
    CNAME docs your-project.pages.dev
    
    8
    SSL/TLS
    9
    SSL certificates are automatically provisioned.

    Build Configuration

    Build Settings

    Configure in Cloudflare Dashboard or wrangler.toml:
    wrangler.toml
    [build]
    command = "npm run build"
    cwd = "."
    watch_dirs = ["content", "app", "components"]
    
    [build.upload]
    format = "directory"
    dir = "out"
    

    Node.js Version

    Specify Node.js version:
    .nvmrc
    20
    
    Or set in dashboard: Settings → Build & Deployments → Node Version

    Image Optimization

    Cloudflare Images integration:

    Option 1: Unoptimized Images

    For static exports:
    next.config.mjs
    const config = {
      output: 'export',
      images: {
        unoptimized: true,
      },
    };
    

    Option 2: Cloudflare Images

    Use Cloudflare’s image optimization:
    next.config.mjs
    const config = {
      images: {
        loader: 'custom',
        loaderFile: './cloudflare-image-loader.js',
      },
    };
    
    cloudflare-image-loader.js
    export default function cloudflareLoader({ src, width, quality }) {
      const params = [`width=${width}`];
      if (quality) {
        params.push(`quality=${quality}`);
      }
      const paramsString = params.join(',');
      return `/cdn-cgi/image/${paramsString}${src}`;
    }
    

    Option 3: Cloudflare Images Service

    For remote images:
    next.config.mjs
    const config = {
      images: {
        domains: ['imagedelivery.net'],
      },
    };
    

    Search Integration

    Static Search (Orama)

    For static exports, use client-side search:
    app/static.json/route.ts
    import { allDocs } from '@/.source';
    import { createSearchAPI } from 'fumadocs-core/search/server';
    
    const api = createSearchAPI('advanced', {
      indexes: allDocs.map((page) => ({
        id: page.url,
        title: page.data.title,
        description: page.data.description,
        url: page.url,
        structuredData: page.data.structuredData,
      })),
    });
    
    export const GET = api.staticGET();
    export const dynamic = 'force-static';
    
    Algolia works seamlessly with Cloudflare Pages:
    components/search.tsx
    import { SearchDialog } from 'fumadocs-ui/components/dialog/search';
    import algoliasearch from 'algoliasearch/lite';
    
    const searchClient = algoliasearch(
      process.env.NEXT_PUBLIC_ALGOLIA_APP_ID!,
      process.env.NEXT_PUBLIC_ALGOLIA_SEARCH_KEY!
    );
    
    export function Search() {
      return (
        <SearchDialog
          search={{
            type: 'algolia',
            client: searchClient,
            index: 'docs',
          }}
        />
      );
    }
    

    Redirects and Headers

    Redirects

    wrangler.toml
    [[redirects]]
    from = "/old-page"
    to = "/new-page"
    status = 301
    
    [[redirects]]
    from = "/docs/*"
    to = "/documentation/:splat"
    status = 302
    
    Or use _redirects file:
    public/_redirects
    /old-page /new-page 301
    /docs/* /documentation/:splat 302
    
    # SPA fallback
    /* /index.html 200
    

    Headers

    Custom headers configuration:
    wrangler.toml
    [[headers]]
    for = "/*"
    [headers.values]
    "X-Frame-Options" = "DENY"
    "X-Content-Type-Options" = "nosniff"
    "Referrer-Policy" = "strict-origin-when-cross-origin"
    
    [[headers]]
    for = "/static/*"
    [headers.values]
    "Cache-Control" = "public, max-age=31536000, immutable"
    
    Or use _headers file:
    public/_headers
    /*
      X-Frame-Options: DENY
      X-Content-Type-Options: nosniff
    
    /static/*
      Cache-Control: public, max-age=31536000, immutable
    

    Performance Optimization

    Caching Strategy

    Cloudflare automatically caches static assets. Configure cache TTL:
    wrangler.toml
    [site]
    bucket = "./out"
    
    [[rules]]
    action = "cache"
    pattern = "*.js"
    cache_ttl = 31536000
    
    [[rules]]
    action = "cache"
    pattern = "*.css"
    cache_ttl = 31536000
    

    Minification

    Enable automatic minification:
    1. Cloudflare Dashboard → Speed → Optimization
    2. Enable:
      • Auto Minify (HTML, CSS, JS)
      • Brotli compression

    HTTP/3 and QUIC

    Enable for faster connections:
    1. Cloudflare Dashboard → Network
    2. Enable HTTP/3 (with QUIC)

    Preview Deployments

    Cloudflare automatically creates preview deployments:
    • Production: Deploys from your default branch
    • Preview: Deploys from pull requests and branches
    Access previews at:
    https://[branch].[project].pages.dev
    

    CI/CD Integration

    GitHub Actions

    .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: 20
              cache: 'npm'
          
          - run: npm ci
          - run: npm run build
          
          - name: Deploy to Cloudflare Pages
            uses: cloudflare/pages-action@v1
            with:
              apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
              accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
              projectName: fumadocs
              directory: out
              gitHubToken: ${{ secrets.GITHUB_TOKEN }}
    

    GitLab CI

    .gitlab-ci.yml
    pages:
      stage: deploy
      image: node:20
      script:
        - npm ci
        - npm run build
        - npx wrangler pages deploy out/
      only:
        - main
      variables:
        CLOUDFLARE_API_TOKEN: $CLOUDFLARE_API_TOKEN
        CLOUDFLARE_ACCOUNT_ID: $CLOUDFLARE_ACCOUNT_ID
    

    Troubleshooting

    Build Failures

    Issue: Build times out or fails Solution:
    1. Check build logs in Cloudflare Dashboard
    2. Verify Node.js version compatibility
    3. Ensure all dependencies are in package.json
    4. Check memory limits (increase if needed)

    404 Errors

    Issue: Pages return 404 after refresh Solution: Add SPA fallback to _redirects:
    /* /index.html 200
    

    Function Size Limits

    Issue: “Worker exceeds size limit” Solution:
    1. Use static export instead of OpenNext
    2. Reduce bundle size:
      npm run build -- --analyze
      
    3. Split large pages or features

    Environment Variables Not Working

    Issue: Environment variables undefined Solution:
    1. Verify variables are set in Cloudflare Dashboard
    2. Use NEXT_PUBLIC_ prefix for client-side variables
    3. Rebuild and redeploy after adding variables
    4. For local development, use .dev.vars

    Monitoring

    Analytics

    Enable Web Analytics:
    1. Cloudflare Dashboard → Analytics → Web Analytics
    2. Add site
    3. Install beacon (automatic for Pages)

    Real User Monitoring

    View performance metrics:
    • Page load times
    • Core Web Vitals
    • Geographic distribution

    Security

    Access Policies

    Restrict access to preview deployments:
    1. Workers & Pages → Your Project → Settings
    2. Access Policies → Add policy
    3. Configure authentication (email, GitHub, etc.)

    Security Headers

    Add recommended security headers:
    public/_headers
    /*
      X-Frame-Options: DENY
      X-Content-Type-Options: nosniff
      X-XSS-Protection: 1; mode=block
      Referrer-Policy: strict-origin-when-cross-origin
      Permissions-Policy: camera=(), microphone=(), geolocation=()
    

    Next Steps

    Build docs developers (and LLMs) love