update skills

This commit is contained in:
2026-03-17 16:53:22 -07:00
parent 0b0783ef8e
commit f9a530667e
389 changed files with 54512 additions and 1 deletions

View File

@@ -0,0 +1,65 @@
# Cloudflare Static Assets Skill Reference
Expert guidance for deploying and configuring static assets with Cloudflare Workers. This skill covers configuration patterns, routing architectures, asset binding usage, and best practices for SPAs, SSG sites, and full-stack applications.
## Quick Start
```jsonc
// wrangler.jsonc
{
"name": "my-app",
"main": "src/index.ts",
"compatibility_date": "2025-01-01",
"assets": {
"directory": "./dist"
}
}
```
```typescript
// src/index.ts
export default {
async fetch(request: Request, env: Env): Promise<Response> {
return env.ASSETS.fetch(request);
}
};
```
Deploy: `wrangler deploy`
## When to Use Workers Static Assets vs Pages
| Factor | Workers Static Assets | Cloudflare Pages |
|--------|----------------------|------------------|
| **Use case** | Hybrid apps (static + dynamic API) | Static sites, SSG |
| **Worker control** | Full control over routing | Limited (Functions) |
| **Configuration** | Code-first, flexible | Git-based, opinionated |
| **Dynamic routing** | Worker-first patterns | Functions (_functions/) |
| **Best for** | Full-stack apps, SPAs with APIs | Jamstack, static docs |
**Decision tree:**
- Need custom routing logic? → Workers Static Assets
- Pure static site or SSG? → Pages
- API routes + SPA? → Workers Static Assets
- Framework (Next, Nuxt, Remix)? → Pages
## Reading Order
1. **configuration.md** - Setup, wrangler.jsonc options, routing patterns
2. **api.md** - ASSETS binding API, request/response handling
3. **patterns.md** - Common patterns (SPA, API routes, auth, A/B testing)
4. **gotchas.md** - Limits, errors, performance tips
## In This Reference
- **[configuration.md](configuration.md)** - Setup, deployment, configuration
- **[api.md](api.md)** - API endpoints, methods, interfaces
- **[patterns.md](patterns.md)** - Common patterns, use cases, examples
- **[gotchas.md](gotchas.md)** - Troubleshooting, best practices, limitations
## See Also
- [Cloudflare Workers Docs](https://developers.cloudflare.com/workers/)
- [Static Assets Docs](https://developers.cloudflare.com/workers/static-assets/)
- [Cloudflare Pages](https://developers.cloudflare.com/pages/)

View File

@@ -0,0 +1,199 @@
# API Reference
## ASSETS Binding
The `ASSETS` binding provides access to static assets via the `Fetcher` interface.
### Type Definition
```typescript
interface Env {
ASSETS: Fetcher;
}
interface Fetcher {
fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>;
}
```
### Method Signatures
```typescript
// 1. Forward entire request
await env.ASSETS.fetch(request);
// 2. String path (hostname ignored, only path matters)
await env.ASSETS.fetch("https://any-host/path/to/asset.png");
// 3. URL object
await env.ASSETS.fetch(new URL("/index.html", request.url));
// 4. Constructed Request object
await env.ASSETS.fetch(new Request(new URL("/logo.png", request.url), {
method: "GET",
headers: request.headers
}));
```
**Key behaviors:**
- Host/origin is ignored for string/URL inputs (only path is used)
- Method must be GET (others return 405)
- Request headers pass through (affects response)
- Returns standard `Response` object
## Request Handling
### Path Resolution
```typescript
// All resolve to same asset:
env.ASSETS.fetch("https://example.com/logo.png")
env.ASSETS.fetch("https://ignored.host/logo.png")
env.ASSETS.fetch("/logo.png")
```
Assets are resolved relative to configured `assets.directory`.
### Headers
Request headers that affect response:
| Header | Effect |
|--------|--------|
| `Accept-Encoding` | Controls compression (gzip, brotli) |
| `Range` | Enables partial content (206 responses) |
| `If-None-Match` | Conditional request via ETag |
| `If-Modified-Since` | Conditional request via modification date |
Custom headers pass through but don't affect asset serving.
### Method Support
| Method | Supported | Response |
|--------|-----------|----------|
| `GET` | ✅ Yes | Asset content |
| `HEAD` | ✅ Yes | Headers only, no body |
| `POST`, `PUT`, etc. | ❌ No | 405 Method Not Allowed |
## Response Behavior
### Content-Type Inference
Automatically set based on file extension:
| Extension | Content-Type |
|-----------|--------------|
| `.html` | `text/html; charset=utf-8` |
| `.css` | `text/css` |
| `.js` | `application/javascript` |
| `.json` | `application/json` |
| `.png` | `image/png` |
| `.jpg`, `.jpeg` | `image/jpeg` |
| `.svg` | `image/svg+xml` |
| `.woff2` | `font/woff2` |
### Default Headers
Responses include:
```
Content-Type: <inferred>
ETag: "<hash>"
Cache-Control: public, max-age=3600
Content-Encoding: br (if supported and beneficial)
```
**Cache-Control defaults:**
- 1 hour (`max-age=3600`) for most assets
- Override via Worker response transformation (see patterns.md:27-35)
### Compression
Automatic compression based on `Accept-Encoding`:
- **Brotli** (`br`): Preferred, best compression
- **Gzip** (`gzip`): Fallback
- **None**: If client doesn't support or asset too small
### ETag Generation
ETags are content-based hashes:
```
ETag: "a3b2c1d4e5f6..."
```
Used for conditional requests (`If-None-Match`). Returns `304 Not Modified` if match.
## Error Responses
| Status | Condition | Behavior |
|--------|-----------|----------|
| `404` | Asset not found | Body depends on `not_found_handling` config |
| `405` | Non-GET/HEAD method | `{ "error": "Method not allowed" }` |
| `416` | Invalid Range header | Range not satisfiable |
### 404 Handling
Depends on configuration (see configuration.md:45-52):
```typescript
// not_found_handling: "single-page-application"
// Returns /index.html with 200 status
// not_found_handling: "404-page"
// Returns /404.html if exists, else 404 response
// not_found_handling: "none"
// Returns 404 response
```
## Advanced Usage
### Modifying Responses
```typescript
const response = await env.ASSETS.fetch(request);
// Clone and modify
return new Response(response.body, {
status: response.status,
headers: {
...Object.fromEntries(response.headers),
'Cache-Control': 'public, max-age=31536000',
'X-Custom': 'value'
}
});
```
See patterns.md:27-35 for full example.
### Error Handling
```typescript
const response = await env.ASSETS.fetch(request);
if (!response.ok) {
// Asset not found or error
return new Response('Custom error page', { status: 404 });
}
return response;
```
### Conditional Serving
```typescript
const url = new URL(request.url);
// Serve different assets based on conditions
if (url.pathname === '/') {
return env.ASSETS.fetch('/index.html');
}
return env.ASSETS.fetch(request);
```
See patterns.md for complete patterns.

View File

@@ -0,0 +1,186 @@
## Configuration
### Basic Setup
Minimal configuration requires only `assets.directory`:
```jsonc
{
"name": "my-worker",
"compatibility_date": "2025-01-01", // Use current date for new projects
"assets": {
"directory": "./dist"
}
}
```
### Full Configuration Options
```jsonc
{
"name": "my-worker",
"main": "src/index.ts",
"compatibility_date": "2025-01-01",
"assets": {
"directory": "./dist",
"binding": "ASSETS",
"not_found_handling": "single-page-application",
"html_handling": "auto-trailing-slash",
"run_worker_first": ["/api/*", "!/api/docs/*"]
}
}
```
**Configuration keys:**
- `directory` (string, required): Path to assets folder (e.g. `./dist`, `./public`, `./build`)
- `binding` (string, optional): Name to access assets in Worker code (e.g. `env.ASSETS`). Default: `"ASSETS"`
- `not_found_handling` (string, optional): Behavior when asset not found
- `"single-page-application"`: Serve `/index.html` for non-asset paths (default for SPAs)
- `"404-page"`: Serve `/404.html` if present, otherwise 404
- `"none"`: Return 404 for missing assets
- `html_handling` (string, optional): URL trailing slash behavior
- `run_worker_first` (boolean | string[], optional): Routes that invoke Worker before checking assets
### not_found_handling Modes
| Mode | Behavior | Use Case |
|------|----------|----------|
| `"single-page-application"` | Serve `/index.html` for non-asset requests | React, Vue, Angular SPAs |
| `"404-page"` | Serve `/404.html` if exists, else 404 | Static sites with custom error page |
| `"none"` | Return 404 for missing assets | API-first or custom routing |
### html_handling Modes
Controls trailing slash behavior for HTML files:
| Mode | `/page` | `/page/` | Use Case |
|------|---------|----------|----------|
| `"auto-trailing-slash"` | Redirect to `/page/` if `/page/index.html` exists | Serve `/page/index.html` | Default, SEO-friendly |
| `"force-trailing-slash"` | Always redirect to `/page/` | Serve if exists | Consistent trailing slashes |
| `"drop-trailing-slash"` | Serve if exists | Redirect to `/page` | Cleaner URLs |
| `"none"` | No modification | No modification | Custom routing logic |
**Default:** `"auto-trailing-slash"`
### run_worker_first Configuration
Controls which requests invoke Worker before checking assets.
**Boolean syntax:**
```jsonc
{
"assets": {
"run_worker_first": true // ALL requests invoke Worker
}
}
```
**Array syntax (recommended):**
```jsonc
{
"assets": {
"run_worker_first": [
"/api/*", // Positive pattern: match API routes
"/admin/*", // Match admin routes
"!/admin/assets/*" // Negative pattern: exclude admin assets
]
}
}
```
**Pattern rules:**
- Glob patterns: `*` (any chars), `**` (any path segments)
- Negative patterns: Prefix with `!` to exclude
- Precedence: Negative patterns override positive patterns
- Default: `false` (assets served directly)
**Decision guidance:**
- Use `true` for API-first apps (few static assets)
- Use array patterns for hybrid apps (APIs + static assets)
- Use `false` for static-first sites (minimal dynamic routes)
### .assetsignore File
Exclude files from upload using `.assetsignore` (same syntax as `.gitignore`):
```
# .assetsignore
_worker.js
*.map
*.md
node_modules/
.git/
```
**Common patterns:**
- `_worker.js` - Exclude Worker code from assets
- `*.map` - Exclude source maps
- `*.md` - Exclude markdown files
- Development artifacts
### Vite Plugin Integration
For Vite-based projects, use `@cloudflare/vite-plugin`:
```typescript
// vite.config.ts
import { defineConfig } from 'vite';
import { cloudflare } from '@cloudflare/vite-plugin';
export default defineConfig({
plugins: [
cloudflare({
assets: {
directory: './dist',
binding: 'ASSETS'
}
})
]
});
```
**Features:**
- Automatic asset detection during dev
- Hot module replacement for assets
- Production build integration
- Requires: Wrangler 4.0.0+, `@cloudflare/vite-plugin` 1.0.0+
### Key Compatibility Dates
| Date | Feature | Impact |
|------|---------|--------|
| `2025-04-01` | Navigation request optimization | SPAs skip Worker for navigation, reducing costs |
Use current date for new projects. See [Compatibility Dates](https://developers.cloudflare.com/workers/configuration/compatibility-dates/) for full list.
### Environment-Specific Configuration
Use `wrangler.jsonc` environments for different configs:
```jsonc
{
"name": "my-worker",
"assets": { "directory": "./dist" },
"env": {
"staging": {
"assets": {
"not_found_handling": "404-page"
}
},
"production": {
"assets": {
"not_found_handling": "single-page-application"
}
}
}
}
```
Deploy with: `wrangler deploy --env staging`

View File

@@ -0,0 +1,162 @@
## Best Practices
### 1. Use Selective Worker-First Routing
Instead of `run_worker_first = true`, use array patterns:
```jsonc
{
"assets": {
"run_worker_first": [
"/api/*", // API routes
"/admin/*", // Admin area
"!/admin/assets/*" // Except admin assets
]
}
}
```
**Benefits:**
- Reduces Worker invocations
- Lowers costs
- Improves asset delivery performance
### 2. Leverage Navigation Request Optimization
For SPAs, use `compatibility_date = "2025-04-01"` or later:
```jsonc
{
"compatibility_date": "2025-04-01",
"assets": {
"not_found_handling": "single-page-application"
}
}
```
Navigation requests skip Worker invocation, reducing costs.
### 3. Type Safety with Bindings
Always type your environment:
```typescript
interface Env {
ASSETS: Fetcher;
}
```
## Common Errors
### "Asset not found"
**Cause:** Asset not in assets directory, wrong path, or assets not deployed
**Solution:** Verify asset exists, check path case-sensitivity, redeploy if needed
### "Worker not invoked for asset"
**Cause:** Asset served directly, `run_worker_first` not configured
**Solution:** Configure `run_worker_first` patterns to include asset routes (see configuration.md:66-106)
### "429 Too Many Requests on free tier"
**Cause:** `run_worker_first` patterns invoke Worker for many requests, hitting free tier limits (100k req/day)
**Solution:** Use more selective patterns with negative exclusions, or upgrade to paid plan
### "Smart Placement increases latency"
**Cause:** `run_worker_first=true` + Smart Placement routes all requests through single smart-placed location
**Solution:** Use selective patterns (array syntax) or disable Smart Placement for asset-heavy apps
### "CF-Cache-Status header unreliable"
**Cause:** Header is probabilistically added for privacy reasons
**Solution:** Don't rely on `CF-Cache-Status` for critical routing logic. Use other signals (ETag, age).
### "JWT expired during deployment"
**Cause:** Large asset deployments exceed JWT token lifetime
**Solution:** Update to Wrangler 4.34.0+ (automatic token refresh), or reduce asset count
### "Cannot use 'assets' with 'site'"
**Cause:** Legacy `site` config conflicts with new `assets` config
**Solution:** Migrate from `site` to `assets` (see configuration.md). Remove `site` key from wrangler.jsonc.
### "Assets not updating after deployment"
**Cause:** Browser or CDN cache serving old assets
**Solution:**
- Hard refresh browser (Cmd+Shift+R / Ctrl+F5)
- Use cache-busting (hashed filenames)
- Verify deployment completed: `wrangler tail`
## Limits
| Resource/Limit | Free | Paid | Notes |
|----------------|------|------|-------|
| Max asset size | 25 MiB | 25 MiB | Per file |
| Total assets | 20,000 | **100,000** | Requires Wrangler 4.34.0+ (Sep 2025) |
| Worker invocations | 100k/day | 10M/month | Optimize with `run_worker_first` patterns |
| Asset storage | Unlimited | Unlimited | Included |
### Version Requirements
| Feature | Minimum Wrangler Version |
|---------|--------------------------|
| 100k file limit (paid) | 4.34.0 |
| Vite plugin | 4.0.0 + @cloudflare/vite-plugin 1.0.0 |
| Navigation optimization | 4.0.0 + compatibility_date: "2025-04-01" |
## Performance Tips
### 1. Use Hashed Filenames
Enable long-term caching with content-hashed filenames:
```
app.a3b2c1d4.js
styles.e5f6g7h8.css
```
Most bundlers (Vite, Webpack, Parcel) do this automatically.
### 2. Minimize Worker Invocations
Serve assets directly when possible:
```jsonc
{
"assets": {
// Only invoke Worker for dynamic routes
"run_worker_first": ["/api/*", "/auth/*"]
}
}
```
### 3. Leverage Browser Cache
Set appropriate `Cache-Control` headers:
```typescript
// Versioned assets
'Cache-Control': 'public, max-age=31536000, immutable'
// HTML (revalidate often)
'Cache-Control': 'public, max-age=0, must-revalidate'
```
See patterns.md:169-189 for implementation.
### 4. Use .assetsignore
Reduce upload time by excluding unnecessary files:
```
*.map
*.md
.DS_Store
node_modules/
```
See configuration.md:107-126 for details.

View File

@@ -0,0 +1,189 @@
### Common Patterns
**1. Forward request to assets:**
```typescript
export default {
async fetch(request: Request, env: Env): Promise<Response> {
return env.ASSETS.fetch(request);
}
};
```
**2. Fetch specific asset by path:**
```typescript
const response = await env.ASSETS.fetch("https://assets.local/logo.png");
```
**3. Modify request before fetching asset:**
```typescript
const url = new URL(request.url);
url.pathname = "/index.html";
return env.ASSETS.fetch(new Request(url, request));
```
**4. Transform asset response:**
```typescript
const response = await env.ASSETS.fetch(request);
const modifiedResponse = new Response(response.body, response);
modifiedResponse.headers.set("X-Custom-Header", "value");
modifiedResponse.headers.set("Cache-Control", "public, max-age=3600");
return modifiedResponse;
```
**5. Conditional asset serving:**
```typescript
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
if (url.pathname === '/') {
return env.ASSETS.fetch('/index.html');
}
return env.ASSETS.fetch(request);
}
};
```
**6. SPA with API routes:**
Most common full-stack pattern - static SPA with backend API:
```typescript
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
if (url.pathname.startsWith('/api/')) {
return handleAPI(request, env);
}
return env.ASSETS.fetch(request);
}
};
async function handleAPI(request: Request, env: Env): Promise<Response> {
return new Response(JSON.stringify({ status: 'ok' }), {
headers: { 'Content-Type': 'application/json' }
});
}
```
**Config:** Set `run_worker_first: ["/api/*"]` (see configuration.md:66-106)
**7. Auth gating for protected assets:**
```typescript
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
if (url.pathname.startsWith('/admin/')) {
const session = await validateSession(request, env);
if (!session) {
return Response.redirect('/login', 302);
}
}
return env.ASSETS.fetch(request);
}
};
```
**Config:** Set `run_worker_first: ["/admin/*"]`
**8. Custom headers for security:**
```typescript
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const response = await env.ASSETS.fetch(request);
const secureResponse = new Response(response.body, response);
secureResponse.headers.set('X-Frame-Options', 'DENY');
secureResponse.headers.set('X-Content-Type-Options', 'nosniff');
secureResponse.headers.set('Content-Security-Policy', "default-src 'self'");
return secureResponse;
}
};
```
**9. A/B testing via cookies:**
```typescript
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const cookies = request.headers.get('Cookie') || '';
const variant = cookies.includes('variant=b') ? 'b' : 'a';
const url = new URL(request.url);
if (url.pathname === '/') {
return env.ASSETS.fetch(`/index-${variant}.html`);
}
return env.ASSETS.fetch(request);
}
};
```
**10. Locale-based routing:**
```typescript
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const locale = request.headers.get('Accept-Language')?.split(',')[0] || 'en';
const url = new URL(request.url);
if (url.pathname === '/') {
return env.ASSETS.fetch(`/${locale}/index.html`);
}
if (!url.pathname.startsWith(`/${locale}/`)) {
url.pathname = `/${locale}${url.pathname}`;
}
return env.ASSETS.fetch(url);
}
};
```
**11. OAuth callback handling:**
```typescript
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
if (url.pathname === '/auth/callback') {
const code = url.searchParams.get('code');
if (code) {
const session = await exchangeCode(code, env);
return new Response(null, {
status: 302,
headers: {
'Location': '/',
'Set-Cookie': `session=${session}; HttpOnly; Secure; SameSite=Lax`
}
});
}
}
return env.ASSETS.fetch(request);
}
};
```
**Config:** Set `run_worker_first: ["/auth/*"]`
**12. Cache control override:**
```typescript
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const response = await env.ASSETS.fetch(request);
const url = new URL(request.url);
// Immutable assets (hashed filenames)
if (/\.[a-f0-9]{8,}\.(js|css|png|jpg)$/.test(url.pathname)) {
return new Response(response.body, {
...response,
headers: {
...Object.fromEntries(response.headers),
'Cache-Control': 'public, max-age=31536000, immutable'
}
});
}
return response;
}
};
```