mirror of
https://github.com/ksyasuda/dotfiles.git
synced 2026-03-21 18:11:27 -07:00
update skills
This commit is contained in:
@@ -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/)
|
||||
199
.agents/skills/cloudflare-deploy/references/static-assets/api.md
Normal file
199
.agents/skills/cloudflare-deploy/references/static-assets/api.md
Normal 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.
|
||||
@@ -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`
|
||||
@@ -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.
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
```
|
||||
Reference in New Issue
Block a user