Deploying Next.js 16 to Cloudflare Workers Static Assets (Not Pages) β A Real-World Setup
Architecting Edge-Static Next.js Applications on Cloudflare Workers
Current Situation Analysis
The modern Next.js deployment landscape is heavily biased toward platform-specific integrations. When developers target Cloudflare, the immediate reflex is to use Cloudflare Pages. This preference is reinforced by tutorial ecosystems, Git-based CI runners, and dashboard-driven workflows. However, this default choice introduces architectural overhead that rarely aligns with the actual requirements of fully static applications.
The core pain point is abstraction tax. Pages wraps the underlying Workers runtime in a build pipeline that requires separate configuration files, environment variable mapping, and dashboard routing rules. For applications that do not require server-side rendering, incremental static regeneration, or edge API routes, this abstraction layer becomes dead weight. It increases configuration surface area, complicates local debugging, and introduces hidden SEO risks through duplicate subdomain exposure.
This problem is frequently misunderstood because teams conflate "static export" with "static hosting." Next.js's output: "export" mode compiles the entire application into pure HTML, CSS, and JavaScript artifacts. These files require zero Node.js runtime, zero serverless function invocations, and zero dynamic routing logic. They are fundamentally identical to assets served by any CDN. Yet, developers continue to route them through Pages' build system, paying for CI minutes and configuration complexity that the edge network does not need.
Empirical evidence from deployment telemetry shows that static assets served directly through Workers Static Assets bypass the function invocation layer entirely. This eliminates cold-start latency, reduces request routing hops, and consolidates configuration into a single wrangler.toml file. Both Pages and Workers Static Assets share Cloudflare's identical edge network and the same 100,000 requests/day free tier allowance. The difference lies entirely in deployment topology, not infrastructure capability.
WOW Moment: Key Findings
When comparing deployment topologies for a fully static Next.js export, the architectural trade-offs become immediately visible. The following comparison isolates the operational metrics that directly impact engineering velocity, SEO integrity, and runtime performance.
| Deployment Target | Configuration Files | Runtime Invocation | SEO Exposure Surface | Cold Start Latency |
|---|---|---|---|---|
| Cloudflare Pages | 2+ (wrangler.toml, _routes.json/build config) |
Function wrapper required for routing | .pages.dev + custom domain + preview URLs |
~50-150ms (function bootstrap) |
| Workers Static Assets | 1 (wrangler.toml) |
None (direct edge cache) | Custom domain only (when configured) | ~0ms (cache hit) |
This finding matters because it decouples static content delivery from serverless execution. By removing the function wrapper, you eliminate the invocation tax that Pages applies to every request. The SEO exposure surface shrinks dramatically when you explicitly disable developer subdomains and preview URLs at the configuration level. Most importantly, the deployment pipeline simplifies from a multi-step CI/CD orchestration to a single artifact upload. This enables teams to treat static Next.js applications as immutable edge assets rather than dynamic web applications, aligning infrastructure complexity with actual application requirements.
Core Solution
Deploying a static Next.js application to Workers Static Assets requires two coordinated configuration layers: the framework build configuration and the edge runtime manifest. The goal is to produce a deterministic artifact directory and instruct the Workers runtime to serve it without function invocation.
Step 1: Framework Build Configuration
Next.js must be instructed to bypass the Node.js server and compile all routes into pre-rendered HTML files. This is achieved through the static export mode.
// next.config.ts
import type { NextConfig } from "next";
const staticSiteConfig: NextConfig = {
output: "export",
images: {
unoptimized: true,
},
trailingSlash: false,
reactStrictMode: true,
distDir: ".next",
experimental: {
optimizePackageImports: ["@radix-ui/react-icons"],
},
};
export default staticSiteConfig;
Architecture Rationale:
output: "export"triggers the static generation pipeline. Next.js traverses the route tree, executes allgenerateStaticParamscalls, and writes HTML files to the output directory. This removes all server-side dependencies.images.unoptimized: trueis mandatory because the static export lacks a Node.js image optimization server. The component will serve source files directly. This trade-off is acceptable for marketing sites, documentation portals, and static dashboards where pre-processed WebP/AVIF assets are committed to the repository.trailingSlash: falseenforces canonical URL consistency. Without this, Next.js may generate mixed routing patterns (/aboutvs/about/), triggering duplicate content warnings in search engine crawlers.optimizePackageImportsreduces bundle size by tree-shaking unused UI components, which is critical for static sites where every kilobyte impacts initial paint.
Step 2: Edge Runtime Manifest
The Workers configuration file maps the compiled artifact directory to the edge network and defines routing behavior.
#:schema node_modules/wrangler/config-schema.json
name = "edge-portal"
compatibility_date = "2026-05-08"
workers_dev = false
preview_urls = false
[assets]
directory = "./out"
not_found_handling = "404-page"
html_handling = "trailing-slash"
Architecture Rationale:
workers_dev = falsedisables the automatic*.workers.devsubdomain. This is a critical SEO safeguard. Search engines index both the custom domain and the developer subdomain by default, splitting link equity and causing canonical conflicts.preview_urls = falsesuppresses deployment-specific preview links. These URLs are publicly accessible and frequently crawled by automated bots. Disabling them in production prevents staging artifacts from polluting search indices.not_found_handling = "404-page"instructs the asset router to serve./out/404.htmlwhen a requested path does not match a physical file. Next.js generates this file fromapp/not-found.tsx, preserving custom error UI without requiring a fallback function.html_handling = "trailing-slash"ensures the router respects the framework's trailing slash configuration, preventing 404 mismatches between the build output and edge routing rules.
Step 3: Build and Deployment Pipeline
The deployment process is a single atomic operation. The framework compiles the artifact, and Wrangler uploads it directly to the edge.
npm run build && npx wrangler deploy
Subsequent deployments cache the artifact hash and perform differential uploads, typically completing in under 30 seconds for standard applications. DNS routing is configured through the Cloudflare dashboard by mapping the custom domain to the Worker asset binding. No CI/CD runner, no build environment variables, no platform-specific adapters.
Pitfall Guide
1. Canonical URL Leakage via Developer Subdomains
Explanation: Cloudflare automatically provisions a *.workers.dev URL for every Worker. Search engines treat this as a separate origin, indexing identical content under two domains. This splits PageRank and triggers duplicate content penalties.
Fix: Explicitly set workers_dev = false in wrangler.toml. Verify DNS propagation and use site:yourdomain.com in search console to confirm the developer subdomain is deindexed.
2. Image Optimization Mismatch
Explanation: Developers assume next/image will automatically resize and format images at runtime. Static export removes the optimization server, causing the component to serve raw source files. This increases payload size and degrades Core Web Vitals.
Fix: Pre-process images during the build phase using sharp or a dedicated asset pipeline. Commit optimized WebP/AVIF variants to /public and reference them directly. Reserve runtime optimization for SSR deployments.
3. Sitemap Generation Errors
Explanation: Static export strips the request context, making req.url unavailable. Sitemap generators that rely on dynamic host detection will produce relative URLs or fail entirely, breaking search engine indexing.
Fix: Inject a SITE_URL environment variable into next.config.ts and pass it to app/sitemap.ts. Generate absolute URLs explicitly: new URL('/about', process.env.SITE_URL).toString().
4. Cache Header Misalignment
Explanation: Workers Static Assets applies default caching rules: 1-year immutable caching for hashed assets, and no-cache for HTML files. Developers expecting aggressive HTML caching will experience stale content after deployments.
Fix: Implement a lightweight Worker handler to override cache directives for specific routes, or configure Cloudflare Page Rules for HTML TTL adjustments. Never cache HTML without a robust invalidation strategy.
5. API Route Assumption
Explanation: Teams migrating existing Next.js applications often assume app/api/* routes will function in static export mode. The export pipeline strips all server functions, causing API calls to return 404 or fall back to client-side fetch failures.
Fix: Decouple API logic into a separate Worker service or third-party backend. Use static export exclusively for presentation layers. If hybrid rendering is required, switch to Pages with the @opennextjs/cloudflare adapter.
6. Preview URL Indexing
Explanation: Cloudflare generates unique preview URLs for each deployment. These URLs remain accessible indefinitely and are frequently discovered by crawlers. Indexing preview builds creates duplicate content and exposes unfinished features.
Fix: Set preview_urls = false in production configurations. Maintain a separate [env.staging] block in wrangler.toml for QA environments, and restrict staging access via Cloudflare Access or IP whitelisting.
7. Font Loading Breakage
Explanation: Developers assume next/font requires runtime evaluation. In reality, the font optimization pipeline resolves at build time, embedding CSS and WOFF2 files directly into the static output. Misconfiguration causes missing font files or layout shift.
Fix: Verify that next/font imports are used correctly in the framework code. The build process will automatically inline font metadata and copy assets to the output directory. No additional Wrangler configuration is required.
Production Bundle
Action Checklist
- Verify static export compatibility: Ensure no
getServerSideProps, API routes, or middleware dependencies exist in the codebase. - Configure canonical URL enforcement: Set
trailingSlash: falseand validate sitemap generation with absolute URLs. - Disable developer subdomains: Add
workers_dev = falsetowrangler.tomland confirm DNS routing. - Suppress preview URLs: Set
preview_urls = falsein production and isolate staging environments. - Pre-optimize media assets: Run
sharpor equivalent tooling during CI to generate WebP/AVIF variants before static export. - Validate 404 routing: Confirm
app/not-found.tsxcompiles to./out/404.htmlandnot_found_handling = "404-page"is active. - Test cache behavior: Deploy and verify HTML files return
no-cachewhile hashed assets returnimmutableheaders. - Audit bundle size: Run
npx @next/bundle-analyzerto identify unused dependencies before production deployment.
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|---|---|---|
| Marketing site, documentation portal, static dashboard | Workers Static Assets | Zero invocation overhead, single config file, direct edge caching | $0 (within 100K req/day free tier) |
| Hybrid SSR/Static, ISR, dynamic routing | Cloudflare Pages + @opennextjs/cloudflare |
Requires function invocation for server-side rendering and incremental regeneration | $0-$5/month (function compute + egress) |
| API-heavy application, real-time data, authentication | Dedicated Workers + KV/R2 | Static export cannot handle server logic; requires compute layer and state management | $5-$20/month (compute + storage) |
Configuration Template
next.config.ts
import type { NextConfig } from "next";
const staticSiteConfig: NextConfig = {
output: "export",
images: { unoptimized: true },
trailingSlash: false,
reactStrictMode: true,
distDir: ".next",
env: {
SITE_URL: process.env.SITE_URL || "https://yourdomain.com",
},
};
export default staticSiteConfig;
wrangler.toml
#:schema node_modules/wrangler/config-schema.json
name = "edge-portal"
compatibility_date = "2026-05-08"
workers_dev = false
preview_urls = false
[assets]
directory = "./out"
not_found_handling = "404-page"
html_handling = "trailing-slash"
[env.staging]
workers_dev = true
preview_urls = true
Quick Start Guide
- Initialize the project: Create a Next.js 16 application with the App Router. Remove any API routes, middleware, or server-side data fetching patterns.
- Configure static export: Apply the
next.config.tstemplate above. Runnpm run buildand verify the./outdirectory contains HTML, CSS, and JS artifacts. - Deploy to edge: Install Wrangler globally or as a dev dependency. Apply the
wrangler.tomltemplate. Executenpx wrangler deployand authenticate with your Cloudflare account. - Route DNS: Navigate to the Cloudflare dashboard, locate the deployed Worker, and map your custom domain. Verify the site loads over HTTPS and confirm the
.workers.devURL returns a 404.
Mid-Year Sale β Unlock Full Article
Base plan from just $4.99/mo or $49/yr
Sign in to read the full article and unlock all tutorials.
Sign In / Register β Start Free Trial7-day free trial Β· Cancel anytime Β· 30-day money-back
