Back to blog
nextjscloudflareopennextvinextvitewranglerworkersweb-development

Building strandgreengables.co.za with Next.js, OpenNext and Cloudflare

21 September 20265 min read

Strand Green Gables is a guesthouse we took over, and we've been rebuilding its marketing and digital side, old charm reaching new travellers. The website was a big part of that. It had to look the part, load quickly on a phone and not need a server to babysit, so I built it with Next.js, the OpenNext Cloudflare adapter and Wrangler, running on Cloudflare Workers.

Why Next.js on Cloudflare Workers

It's mostly content: photos, room descriptions, a location section, some things to do and a blog with local guides. Bookings go through Airbnb, so there's no booking engine, no database and no accounts to build. That suits an edge hosted app well, and Next.js still gives me file based routing and good metadata and sitemap tooling for the SEO side.

Next.js is built around Node.js and Vercel, and Cloudflare Workers isn't Node.js, it's a V8 isolate runtime with its own APIs. OpenNext is the adapter that bridges the two. It takes the output of next build and repackages it as a Worker plus a folder of static assets, which is exactly what Wrangler knows how to deploy.

Setting up OpenNext and Wrangler

Step 1: Install the adapter and Wrangler as dev dependencies

pnpm add -D @opennextjs/cloudflare wrangler

Step 2: Add an open-next.config.ts, the defaults are enough for a content site

import { defineCloudflareConfig } from "@opennextjs/cloudflare"
export default defineCloudflareConfig()

Step 3: Describe the Worker in wrangler.jsonc

{
  "$schema": "node_modules/wrangler/config-schema.json",
  "name": "strandgreengables",
  "main": ".open-next/worker.js",
  "compatibility_date": "2026-06-12",
  "compatibility_flags": ["nodejs_compat"],
  "observability": { "enabled": true },
  "assets": {
    "directory": ".open-next/assets",
    "binding": "ASSETS"
  }
}

main is the Worker OpenNext generates during the build, so there's no need to write it by hand, and assets serves the static output (_next/static, images, fonts) straight from Cloudflare without invoking the Worker. nodejs_compat is needed because Next.js and some of its dependencies expect Node APIs. Leave it out and the Worker fails at runtime instead of at build time, which is a nasty surprise. observability turns on Workers logs. To attach the domain add a routes entry with "custom_domain": true and Cloudflare handles DNS and the certificate.

Step 4: Add the scripts to package.json

{
  "scripts": {
    "dev": "next dev",
    "preview": "opennextjs-cloudflare build && opennextjs-cloudflare preview",
    "deploy": "opennextjs-cloudflare build && opennextjs-cloudflare deploy"
  }
}

next dev is for day to day work, preview builds the real Worker and runs it locally in the Workers runtime, and deploy builds and pushes to Cloudflare. There's also an upload variant that uploads a new version without promoting it, handy for checking something before it goes live.

Step 5: Add this to the bottom of next.config.mjs so next dev knows about Cloudflare bindings

import { initOpenNextCloudflareForDev } from "@opennextjs/cloudflare"
initOpenNextCloudflareForDev()

Things to watch for

A few things to watch for. Run preview before deploy, because a build that works in next dev can still fail in the Workers runtime. The default Next.js image optimiser needs infrastructure behind it, so images: { unoptimized: true } with correctly sized WebP files is the simple route, and Cloudflare Images is there if it ever matters. Keep secrets in .dev.vars locally and set production ones with wrangler secret put. Workers have a size limit on the deployed script, so keep an eye on dependencies (server components help, a content site sends very little JavaScript anyway), and keep middleware light because it runs on every request.

Update: a possible move to vinext

Since writing this, Cloudflare released vinext, and it's worth a look if you're on OpenNext. OpenNext takes the output of next build and adapts it for Workers. vinext doesn't use next build at all, it reimplements the Next.js API surface (App Router, server components, route handlers, middleware, metadata, next/link and friends) as a Vite plugin, with Workers as its main target. Your app/ directory, next.config and public/ stay as they are, but the build, the dev server and the Worker underneath change completely.

I've moved this site over to it as a trial run before touching Strand Green Gables, and for a content site the migration is small. Here's roughly what it looks like.

Step 1: Check what vinext thinks of your project

npx vinext check

It lists every next/* import, config option and library with a supported, partial or unsupported verdict. Read the partial ones properly, that's where the surprises are.

Step 2: Let vinext init do the setup

npx vinext init --platform=cloudflare

It installs vinext, Vite and the Cloudflare Vite plugin, adds "type": "module" to package.json and generates a vite.config.ts. New projects get a typed cloudflare.config.ts by default. If you want to keep an existing wrangler.jsonc with its routes and custom domains, add --legacy-wrangler-cloudflare-init. It also asks whether you want a CDN cache, a KV data cache and Cloudflare Images. For a site that ran on the OpenNext defaults with unoptimised images, choosing none for all three keeps behaviour the same.

Step 3: Point Wrangler at vinext instead of .open-next

{
  "main": "vinext/server/fetch-handler",
  "assets": {
    "directory": "dist/client",
    "not_found_handling": "none",
    "binding": "ASSETS"
  }
}

Everything else in the file (compatibility flags, observability, routes) carries over. Remove the WORKER_SELF_REFERENCE service binding if you had one, only OpenNext uses it.

Step 4: Swap the scripts and remove OpenNext

{
  "scripts": {
    "dev": "vite dev",
    "build": "vite build",
    "preview": "vite build && wrangler dev --config dist/server/wrangler.json",
    "deploy": "vinext-cloudflare deploy --config dist/server/wrangler.json"
  }
}

Then delete open-next.config.ts, the initOpenNextCloudflareForDev() call in next.config.mjs and the @opennextjs/cloudflare package. The next package can go too, vinext ships its own types for next/* imports, so add "vinext/types" to types in tsconfig.json. In dev the server code already runs in workerd, so bindings come from import { env } from "cloudflare:workers" with no extra setup.

A few things I ran into:

  • Webpack and Turbopack loaders are ignored. I imported blog posts as Markdown through raw-loader, and on Vite that rule is ignored (vinext check does flag it). Vite's own ?raw suffix (import post from "./post.md?raw") does the same job.
  • Some metadata shapes aren't supported yet. The [{ url, title }] form of alternates.types for an RSS link crashed every page that used it. Rendering the tag directly in the root layout fixed it.
  • Peer versions are strict. The Cloudflare Vite plugin refused to load with an older Wrangler, so expect to upgrade Wrangler alongside it.
  • Run preview before deploying, same as with OpenNext. Pages that worked in vite dev still need checking in the real Worker.

vinext is still under active development. Cache Components and Partial Prerendering are incomplete, and there's no build-time image optimisation, so it isn't a drop-in replacement for every app yet. For a small, mostly static site like this one or Strand Green Gables it's a reasonable trade. There are fewer moving parts between your code and the Worker, and you're on plain Vite instead of an adapter sitting on top of next build. If your app leans on newer Next.js caching features, stick with OpenNext for now and run vinext check again in a few months.

The result

For a content site like this it removes most of the moving parts. There's no server to patch, deploys are one command and rollbacks are a click in the Cloudflare dashboard. If this helps anyone deploying Next.js to Cloudflare, great, and the OpenNext Cloudflare docs, vinext docs and Wrangler docs are the place to go for the rest. If you're ever near Strand, the site is at strandgreengables.co.za.