NextJS on Cloudflare Wiki

Published on

Initialize, write and deploy NextJS applications optimized for running on Cloudflare.

Platforma
  • Next.js is an open-source web development framework created by the private company Vercel providing React-based web applications with server-side rendering and static rendering, see more at Next.JS page.
  • Cloudflare is one of the biggest network service for the purposes of increasing the security and performance of web sites and services. It also provides a cloud service to host files and applications.

Initialize a NextJs Worker app for Cloudflare:

npm create cloudflare@latest -- my-next-app --framework=next --platform=workers
  • You're in an existing git repository. Do you want to use git for version control? - choose Yes
  • Do you want to deploy your application? choose Yes or No based on your preference.

Run the app on localhost options:

  1. npm run dev
  2. npx wrangler dev

Deploy / Redeploy (from Ubuntu WSL on Windows)

NOTE: If you need to deploy applications to more Cloudflare accounts, use Deploying with a token.

  1. Be sure you are authentizated / use proper token
    npx wrangler whoami
  2. Build the app for Cloudflare Worker
    npx @opennextjs/cloudflare build
  3. npx wrangler deploy

Check real-time logs on deployed ver through wrangler

wrangler tail

Creating D1 database and connecting it

Either throgh Cloudflare UI, or through wrangler in terminal - In project root folder terminal, run

npx wrangler d1 create my-next-app-db

D1 Typing

npx wrangler types --env-interface CloudflareEnv

Connect DB

  • In wrangler.jsonc, add
    "d1_databases": [
            {
                "binding": "my_db",
                "database_name": "my-next-app-db",
                "database_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
                "remote": true
            }
        ]
  • Then in any script you need the db, or in a lib
    import { getCloudflareContext } from "@opennextjs/cloudflare";
    
    type Env = { my_db: D1Database; // binding name in wrangler.jsonc };
    
    export function getD1() {
      const { env } = getCloudflareContext() as unknown as { env: Env };
      const db = env.my_db;
    }

Integration R2 Storage Database to NextJS application

R2 Storage Database can be configured in CloudFlare UI: Storage & Databases / R2 Object Storage. Alternatively, you can create R2 storage database also through cli, see Create new buckets.

R2 Storage can be used for holding any type of files, from common web media up to cache jsons.

Config wrangler.jsonc

"r2_buckets": [
    {
        "binding": "NEXT_INC_CACHE_R2_BUCKET",
        "bucket_name": "cache"
    }
],

Sample R2 Library src/lib/r2.ts

import { getCloudflareContext } from "@opennextjs/cloudflare";

type Env = { NEXT_INC_CACHE_R2_BUCKET: R2Bucket; };
   
const r2CacheKey : string = 'vrealmatic_page/v3/';
       
async function getBucket(): Promise<R2Bucket> {
    const ctx = getCloudflareContext() as { env: Env };
    const bucket = ctx.env.NEXT_INC_CACHE_R2_BUCKET;
    if (!bucket) throw new Error("R2 binding NEXT_INC_CACHE_R2_BUCKET is missing in Cloudflare context env.");
    return bucket;
}
            
export async function r2GetJson<T>(key: string): Promise<T | null> {
    const bucket = await getBucket();
    const obj = await bucket.get(r2CacheKey+key);
    if (!obj) return null;
    return await obj.json<T>();
}
            
export async function r2PutJson(key: string, value: unknown): Promise<void> {
    const bucket = await getBucket();
    await bucket.put((r2CacheKey+key), JSON.stringify(value), { httpMetadata: { contentType: "application/json" } });
}

Use Sample for saving cache data to R2

type R2Cached<T> = {
    ts: number;     // stored at (ms)
    data: T;
};
                    
const PAGE_CTX_TTL_MS = 86_400_000; // 24h
                    
const inFlight = new Map<string, Promise<PageContext>>();
async function getRequestedAndAlternates_TTL(urlPath: string): Promise<PageContext> {
                    
    const existing = inFlight.get(urlPath);
    if (existing) return existing;
                      
    const p = (async () => {
        try {
          const now = Date.now();
                    
          // 1) try cache
          const cached = await r2GetJson<R2Cached<PageContext>>(urlPath);
          if ( cached && (now - cached.ts) < PAGE_CTX_TTL_MS ) return cached.data;
          
          // 2) MISS/STALE => DB
          const data: PageContext = await GetPageByUrlAndAlternates(urlPath);
                          
          // 3) Write / update to cache, if the page exists
          if(data.page) await r2PutJson(urlPath, { ts: now, data } satisfies R2Cached<PageContext>);
                    
          return data;
        } finally {
          inFlight.delete(urlPath);
        }
      })();
                    
      inFlight.set(urlPath, p);
      return p;
    }
                    
    export function getPageContext(slug?: string[]) {
      const baseUrlPath = (!slug || slug.length === 0) ? "" : slug.join("/");
      return getPageContextCache(baseUrlPath);
    }
                    
    export const getPageContextCache = cache(getRequestedAndAlternates_TTL);

Incremental cache for NextJS on Cloudflare

  1. At wrangler.jsonc, add bindings to R2 bucket and Durable Object + migration, such as:
    "r2_buckets": [
    		{
    			"binding": "NEXT_INC_CACHE_R2_BUCKET",
    			"bucket_name": "inc-cache"
    		},
    		//...
    	],
    "durable_objects": {
    		"bindings": [
    			{
    				"name": "NEXT_CACHE_DO_QUEUE",
    				"class_name": "DOQueueHandler"
    			}
    		]
    	},
    "migrations": [
    		{ "tag": "v1", "new_sqlite_classes": ["DOQueueHandler"] },
    	],
  2. At open-next.config.ts, attach incremenatl cache configuration, such as:
    import { defineCloudflareConfig } from "@opennextjs/cloudflare";
    import r2IncrementalCache from "@opennextjs/cloudflare/overrides/incremental-cache/r2-incremental-cache";
    import { withRegionalCache } from "@opennextjs/cloudflare/overrides/incremental-cache/regional-cache";
    import doQueue from "@opennextjs/cloudflare/overrides/queue/do-queue";
    
    export default defineCloudflareConfig({
    	// See https://opennext.js.org/cloudflare/caching for more details
    	incrementalCache: withRegionalCache(r2IncrementalCache, { mode: "long-lived" }), 
    	queue: doQueue,
    });
  3. Expand your worker-entry.mjs for DO
    import openNextWorker from "./.open-next/worker.js";
    export { DOQueueHandler } from "./.open-next/worker.js";
    
    export default {
      async fetch(request, env, ctx) {
        const url = new URL(request.url);
    
        if (url.pathname === "/en" || url.pathname === "/en/") {
          url.pathname = "/";
          return Response.redirect(url.href, 308);
        }
    
        if (url.hostname === "xxx.com") {
          url.hostname = "www.xxx.com";
          url.protocol = "https:";
          return Response.redirect(url.href, 308);
        }
    
        if (url.hostname.endsWith(".workers.dev")) {
          url.hostname = "www.xxx.com";
          url.protocol = "https:";
          return Response.redirect(url.href, 308);
        }
    
        return withEdgeCache(request, ctx, (req) => openNextWorker.fetch(req, env, ctx));
      },
    };
    
  4. Build & Deploy

This mechanism uses default doQueue from "@opennextjs/cloudflare/overrides/queue/do-queue". DO is created automatically. There is nothing next to do above the steps described above.

This mechanism eliminates observability errors of type "Failed to revalidate stale page ... FatalError: Dummy queue is not implemented".

Vrealmatic consulting

Anything unclear?

Let us know!

Contact Us