If you've shipped a Next.js app to Cloudflare in the last few years, you probably reached for Cloudflare Pages. It's the default answer in every tutorial, the Vercel-shaped slot in the Cloudflare ecosystem.
But in late 2025, Cloudflare quietly pushed Workers Static Assets as a first-class deployment target — and for a static-exported Next.js app, it's now the better choice. Smaller config, faster cold starts, and one fewer abstraction layer between your out/ folder and the edge.
This post walks through the exact setup I used to ship a real production Next.js 16 + React 19 site to Workers Static Assets in under 90 seconds — including the three wrangler.toml fields most tutorials skip.
Pages vs Workers Static Assets — Quick Context
Pages was built around a Git-integration-first workflow with a built-in CI runner. Workers Static Assets ships your built artifact directly to the Workers runtime — no separate Pages dashboard, no Pages-specific build configuration, just wrangler deploy.
For a fully static Next.js export (no SSR, no API routes), Workers Static Assets is lighter:
- One config file (
wrangler.toml), not two systems - Same edge network, same free tier (100K requests/day)
- Cleaner integration if you later need a Workers function alongside the assets
If your app uses SSR, ISR, or middleware, Pages or the newer @opennextjs/cloudflare adapter is still the right call. This post is for the static-export case.
TL;DR — The Full Config
Here's everything you need. Two files.
next.config.ts
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
output: "export",
images: { unoptimized: true },
trailingSlash: false,
reactStrictMode: true,
};
export default nextConfig;
wrangler.toml
#:schema node_modules/wrangler/config-schema.json
name = "my-app"
compatibility_date = "2026-05-08"
workers_dev = false
preview_urls = false
[assets]
directory = "./out"
not_found_handling = "404-page"
That's it. Run pnpm build then npx wrangler deploy, point your DNS at the Worker, and you're live. The rest of this post is why each line is there.
next.config.ts — Three Switches That Matter
output: "export"
This is the switch that turns Next.js into a static site generator. Every route becomes a pre-rendered HTML file in ./out/, with no Node runtime required. Workers Static Assets serves them directly.
What you lose: app/api routes, ISR, next/image runtime optimization, and middleware. What you gain: a build artifact that runs on any static host on the planet.
images: { unoptimized: true }
next/image needs a Node runtime to resize and reformat on the fly. Static export doesn't have one. Setting unoptimized: true tells the component to serve your image as-is.
The trade-off: you give up automatic AVIF/WebP conversion and per-viewport sizing. The fix: pre-process your images with sharp at build time, or use Cloudflare Images (the paid product) if you really need it. For most static sites, just shipping pre-optimized PNG/WebP from /public is fine.
trailingSlash: false
This one is a canonical hygiene issue, not a feature. With trailingSlash: false, every internal link, sitemap entry, and Open Graph URL ends without a /. Mix and match across pages and you'll get duplicate-content warnings in Google Search Console within a week.
Pick one, set it here, and never think about it again.
wrangler.toml — Three Fields Most Tutorials Skip
This is where the "I read the docs and it still didn't work" hours live.
not_found_handling = "404-page"
By default, Workers Static Assets returns a generic JSON 404 when a path isn't found. That's fine for an API, terrible for an app where you carefully wrote a not-found.tsx page in Next.js.
Setting not_found_handling = "404-page" tells the runtime: "if the path doesn't exist, look for ./out/404.html and serve that instead." Next.js generates that file automatically from your not-found.tsx.
workers_dev = false ⭐
This is the one I want every SEO-conscious developer to know.
By default, your Worker is reachable at both your-app.com (your custom domain) and my-app.workers.dev (Cloudflare's free subdomain). Google can and does index both. Result: duplicate content, split PageRank, and a workers.dev URL outranking your real domain in long-tail searches.
Setting workers_dev = false kills the .workers.dev URL entirely. Your app is only reachable on your custom domain. One canonical URL, no leakage.
preview_urls = false
Same idea, different surface. Cloudflare gives every deployment a preview URL like <hash>-<worker>.preview-domain.workers.dev. Useful for QA. Also indexable by Google if you're unlucky.
Set this to false in production. Use a separate [env.preview] block in wrangler.toml if you want previews on staging without leaking them on prod.
Deploy in One Line
pnpm build && npx wrangler deploy
First deploy walks you through OAuth, picks an account, and uploads ./out/. Subsequent deploys take ~30 seconds for a typical app.
Point your DNS at the Worker via the Cloudflare dashboard (Workers & Pages → your Worker → Custom Domains), and you're live on your own domain.
Common Gotchas
A short list of things that cost me time, so they don't cost you any:
-
Sitemap absolute URLs: Static export means every URL in your sitemap must be absolute. Use a
SITE_URLenv var innext.config.tsand reference it inapp/sitemap.ts. -
No
app/api: If you have API routes, either move them to a separate Worker, use a third-party form/auth backend, or switch to Pages with the Functions adapter. -
next/fontworks: Despite being a "runtime" feature in name,next/fontresolves at build time and ships static CSS/woff2 with your export. No config needed. - Cache headers: Workers Static Assets sets sensible defaults (1 year for hashed assets, no-cache for HTML). Override with a Worker handler if you need custom logic.
-
pnpmvsnpm: Wrangler doesn't care. Use whatever you build with.
What I Built With This Setup
I used the exact config above to ship Toon Tone — a daily cartoon color memory game running on Next.js 16, React 19, and Tailwind 4. First production deploy took 87 seconds. Ongoing cost: $0 on Cloudflare's free tier.
If you want to see the result live, the daily game is at toontone.win and the HSB color-match page is the one that uses every feature mentioned above — static export, edge caching, custom 404, the lot.
Happy to answer setup questions in the comments — especially around the workers_dev = false SEO gotcha, since it's not in the official docs and bit me on a previous project before this one.


Top comments (0)