static-assets
Cloudflare Static Assets
Serve static files, single-page applications (SPAs), and build hybrid Workers that combine API routes with frontend hosting.
FIRST: Project Setup
Static Assets require a public/ directory and configuration in wrangler.jsonc:
# Create public directory for your build output
mkdir public
# Add your static files
echo "<h1>Hello World</h1>" > public/index.html
When to Use
| Use Case | Description |
|---|---|
| Single-Page Applications | React, Vue, Angular apps with client-side routing |
| Static Sites | HTML/CSS/JS sites, generated static content |
| Hybrid Apps | Worker API routes + frontend in one deployment |
| Framework Integration | Next.js, Vite, Remix build outputs |
Quick Reference
| Task | Configuration |
|---|---|
| Basic static hosting | "assets": { "directory": "./public/" } |
| SPA routing (404 → index) | "not_found_handling": "single-page-application" |
| Custom 404 page | "not_found_handling": "404-page" |
| Custom binding name | "binding": "ASSETS" (default) |
| Access in Worker | env.ASSETS.fetch(request) |
wrangler.jsonc Configuration
Basic Static Hosting
{
"name": "my-static-site",
"compatibility_date": "2024-01-01",
"assets": {
"directory": "./public/"
}
}
SPA with Custom 404 Handling
{
"name": "my-spa",
"compatibility_date": "2024-01-01",
"assets": {
"directory": "./public/",
"not_found_handling": "single-page-application"
}
}
not_found_handling options:
"single-page-application"- Routes all 404s to/index.html(default for SPAs)"404-page"- Serves/404.htmlfor missing files"none"- Returns standard 404 responses
Hybrid Worker with Static Assets
Combine API routes with static file serving in a single Worker:
// src/index.ts
interface Env {
ASSETS: Fetcher;
}
export default {
fetch(request, env) {
const url = new URL(request.url);
// Handle API routes in the Worker
if (url.pathname.startsWith("/api/")) {
return Response.json({
name: "Cloudflare",
timestamp: Date.now()
});
}
// Serve static assets for all other routes
return env.ASSETS.fetch(request);
},
} satisfies ExportedHandler<Env>;
wrangler.jsonc for hybrid app:
{
"name": "my-app",
"main": "src/index.ts",
"compatibility_date": "2024-01-01",
"assets": {
"directory": "./public/",
"not_found_handling": "single-page-application",
"binding": "ASSETS"
},
"observability": {
"enabled": true
}
}
SPA Routing
Single-page applications need all routes to resolve to index.html for client-side routing to work:
{
"assets": {
"directory": "./public/",
"not_found_handling": "single-page-application"
}
}
What this does:
/→ serves/index.html/about→ serves/index.html(client-side router handles /about)/contact→ serves/index.html/styles.css→ serves actual file if it exists- Missing file → serves
/index.html
API Routes Pattern
Common pattern: Handle API requests in Worker, serve everything else as static assets:
interface Env {
ASSETS: Fetcher;
DB: D1Database;
}
export default {
async fetch(request, env) {
const url = new URL(request.url);
// API Routes
if (url.pathname.startsWith("/api/")) {
switch (url.pathname) {
case "/api/users":
const users = await env.DB.prepare("SELECT * FROM users").all();
return Response.json(users.results);
case "/api/health":
return Response.json({ status: "ok" });
default:
return Response.json({ error: "Not found" }, { status: 404 });
}
}
// Static Assets
return env.ASSETS.fetch(request);
},
} satisfies ExportedHandler<Env>;
Framework Integration
Vite (React, Vue, Svelte)
# Build output goes to dist/
npm run build
# Move to public/
mv dist/* public/
vite.config.ts:
export default {
build: {
outDir: 'public'
}
}
Next.js (Static Export)
npm run build
next.config.js:
module.exports = {
output: 'export',
distDir: 'public'
}
Remix
npm run build
remix.config.js:
module.exports = {
assetsBuildDirectory: "public/build"
}
ASSETS Fetcher API
The ASSETS binding is a Fetcher that handles static file requests:
interface Env {
ASSETS: Fetcher;
}
// Direct pass-through
env.ASSETS.fetch(request)
// Modify request before serving
const modifiedRequest = new Request(request.url, {
headers: { "Cache-Control": "max-age=3600" }
});
env.ASSETS.fetch(modifiedRequest)
// Serve specific file
env.ASSETS.fetch(new Request("https://example.com/index.html"))
Cache Control
Static assets are cached by default. Customize caching behavior:
export default {
async fetch(request, env) {
const url = new URL(request.url);
// API routes - no cache
if (url.pathname.startsWith("/api/")) {
return Response.json({ data: "dynamic" }, {
headers: { "Cache-Control": "no-store" }
});
}
// Get asset response
const response = await env.ASSETS.fetch(request);
// Customize cache headers for specific files
if (url.pathname.endsWith(".js") || url.pathname.endsWith(".css")) {
const newResponse = new Response(response.body, response);
newResponse.headers.set("Cache-Control", "public, max-age=31536000, immutable");
return newResponse;
}
return response;
},
} satisfies ExportedHandler<Env>;
Detailed References
- references/configuration.md - Full assets configuration options, binding details
- references/frameworks.md - Framework-specific build configurations
- references/testing.md - buildPagesASSETSBinding, testing SPA fallback, ASSETS binding
Best Practices
- Use SPA mode for client-side routing: Set
"not_found_handling": "single-page-application" - Prefix API routes: Use
/api/*pattern for clear separation from static routes - Configure framework output: Point build output directly to
./public/ - Cache immutable assets: Add cache headers for hashed files (
app.abc123.js) - Enable observability: Track Worker analytics with
"observability": { "enabled": true } - Test locally: Use
wrangler devto test both Worker and assets together - Handle errors gracefully: Return proper status codes for API routes vs static assets
Common Patterns
Authentication Check Before Assets
export default {
async fetch(request, env) {
const url = new URL(request.url);
// Public routes
if (url.pathname === "/login" || url.pathname.startsWith("/public/")) {
return env.ASSETS.fetch(request);
}
// Check authentication
const token = request.headers.get("Authorization");
if (!token) {
return new Response("Unauthorized", { status: 401 });
}
// API or authenticated assets
if (url.pathname.startsWith("/api/")) {
return handleAPI(request, env);
}
return env.ASSETS.fetch(request);
},
};
Custom Error Pages
export default {
async fetch(request, env) {
const url = new URL(request.url);
if (url.pathname.startsWith("/api/")) {
try {
return await handleAPI(request, env);
} catch (error) {
return Response.json({ error: error.message }, { status: 500 });
}
}
const response = await env.ASSETS.fetch(request);
// Serve custom 404 page
if (response.status === 404) {
return env.ASSETS.fetch(new Request(`${url.origin}/404.html`));
}
return response;
},
};
Local Development
# Start dev server with assets
wrangler dev
# Specify custom port
wrangler dev --port 8787
# Auto-rebuild on changes (if using framework)
# Terminal 1: Framework watch mode
npm run dev
# Terminal 2: Wrangler
wrangler dev
Deployment
# Deploy Worker + Assets together
wrangler deploy
# View deployment
wrangler deployments list
The Worker and static assets are deployed as a single unit. All files in the assets.directory are uploaded and served from Cloudflare's global network.
More from null-shot/cloudflare-skills
cloudflare-opennext
Deploy Next.js to Cloudflare Workers with full App Router, Pages Router, ISR, and SSG support. Load when creating Next.js projects for Workers, migrating from Vercel/next-on-pages, configuring caching (R2/KV/D1), accessing Cloudflare bindings via getCloudflareContext, or fixing bundle size issues.
196browser-rendering
Headless Chrome automation for web scraping, screenshots, PDFs, and testing at the edge. Load when capturing page screenshots, generating PDFs, scraping dynamic content, extracting structured data, or automating browser interactions. Supports REST API, Puppeteer, Playwright, and Stagehand.
17r2-storage
S3-compatible object storage for files, images, and large data. Load when handling file uploads, storing images/videos/documents, generating presigned URLs, using multipart uploads for large files, migrating from S3, or serving static assets from buckets.
16workers
Core Workers fundamentals including handlers, configuration, and Service Bindings. Load when creating new Workers, configuring wrangler.jsonc, implementing fetch/scheduled/queue handlers, using Service Bindings for RPC, generating types with wrangler types, or building microservices.
10queues
Asynchronous message queues for reliable background processing. Load when offloading background tasks, batch processing messages, implementing retry logic with dead letter queues, rate limiting upstream APIs, or decoupling producers from consumers.
10wrangler
Cloudflare Workers CLI for deploying, developing, and managing Workers, KV, R2, D1, Vectorize, Hyperdrive, Workers AI, Containers, Queues, Workflows, Pipelines, and Secrets Store. Load before running wrangler commands to ensure correct syntax and best practices.
7