mirror of
https://github.com/ksyasuda/dotfiles.git
synced 2026-03-21 18:11:27 -07:00
update skills
This commit is contained in:
61
.agents/skills/cloudflare-deploy/references/images/README.md
Normal file
61
.agents/skills/cloudflare-deploy/references/images/README.md
Normal file
@@ -0,0 +1,61 @@
|
||||
# Cloudflare Images Skill Reference
|
||||
|
||||
**Cloudflare Images** is an end-to-end image management solution providing storage, transformation, optimization, and delivery at scale via Cloudflare's global network.
|
||||
|
||||
## Quick Decision Tree
|
||||
|
||||
**Need to:**
|
||||
- **Transform in Worker?** → [api.md](api.md#workers-binding-api-2026-primary-method) (Workers Binding API)
|
||||
- **Upload from Worker?** → [api.md](api.md#upload-from-worker) (REST API)
|
||||
- **Upload from client?** → [patterns.md](patterns.md#upload-from-client-direct-creator-upload) (Direct Creator Upload)
|
||||
- **Set up variants?** → [configuration.md](configuration.md#variants-configuration)
|
||||
- **Serve responsive images?** → [patterns.md](patterns.md#responsive-images)
|
||||
- **Add watermarks?** → [patterns.md](patterns.md#watermarking)
|
||||
- **Fix errors?** → [gotchas.md](gotchas.md#common-errors)
|
||||
|
||||
## Reading Order
|
||||
|
||||
**For building image upload/transform feature:**
|
||||
1. [configuration.md](configuration.md) - Setup Workers binding
|
||||
2. [api.md](api.md#workers-binding-api-2026-primary-method) - Learn transform API
|
||||
3. [patterns.md](patterns.md#upload-from-client-direct-creator-upload) - Direct upload pattern
|
||||
4. [gotchas.md](gotchas.md) - Check limits and errors
|
||||
|
||||
**For URL-based transforms:**
|
||||
1. [configuration.md](configuration.md#variants-configuration) - Create variants
|
||||
2. [api.md](api.md#url-transform-api) - URL syntax
|
||||
3. [patterns.md](patterns.md#responsive-images) - Responsive patterns
|
||||
|
||||
**For troubleshooting:**
|
||||
1. [gotchas.md](gotchas.md#common-errors) - Error messages
|
||||
2. [gotchas.md](gotchas.md#limits) - Size/format limits
|
||||
|
||||
## Core Methods
|
||||
|
||||
| Method | Use Case | Location |
|
||||
|--------|----------|----------|
|
||||
| `env.IMAGES.input().transform()` | Transform in Worker | [api.md:11](api.md) |
|
||||
| REST API `/images/v1` | Upload images | [api.md:57](api.md) |
|
||||
| Direct Creator Upload | Client-side upload | [api.md:127](api.md) |
|
||||
| URL transforms | Static image delivery | [api.md:112](api.md) |
|
||||
|
||||
## In This Reference
|
||||
|
||||
- **[api.md](api.md)** - Complete API: Workers binding, REST endpoints, URL transforms
|
||||
- **[configuration.md](configuration.md)** - Setup: wrangler.toml, variants, auth, signed URLs
|
||||
- **[patterns.md](patterns.md)** - Patterns: responsive images, watermarks, format negotiation, caching
|
||||
- **[gotchas.md](gotchas.md)** - Troubleshooting: limits, errors, best practices
|
||||
|
||||
## Key Features
|
||||
|
||||
- **Automatic Optimization** - AVIF/WebP format negotiation
|
||||
- **On-the-fly Transforms** - Resize, crop, blur, sharpen via URL or API
|
||||
- **Workers Binding** - Transform images in Workers (2026 primary method)
|
||||
- **Direct Upload** - Secure client-side uploads without backend proxy
|
||||
- **Global Delivery** - Cached at 300+ Cloudflare data centers
|
||||
- **Watermarking** - Overlay images programmatically
|
||||
|
||||
## See Also
|
||||
|
||||
- [Official Docs](https://developers.cloudflare.com/images/)
|
||||
- [Workers Examples](https://developers.cloudflare.com/images/tutorials/)
|
||||
96
.agents/skills/cloudflare-deploy/references/images/api.md
Normal file
96
.agents/skills/cloudflare-deploy/references/images/api.md
Normal file
@@ -0,0 +1,96 @@
|
||||
# API Reference
|
||||
|
||||
## Workers Binding API
|
||||
|
||||
```toml
|
||||
# wrangler.toml
|
||||
[images]
|
||||
binding = "IMAGES"
|
||||
```
|
||||
|
||||
### Transform Images
|
||||
|
||||
```typescript
|
||||
const imageResponse = await env.IMAGES
|
||||
.input(fileBuffer)
|
||||
.transform({ width: 800, height: 600, fit: "cover", quality: 85, format: "avif" })
|
||||
.output();
|
||||
return imageResponse.response();
|
||||
```
|
||||
|
||||
### Transform Options
|
||||
|
||||
```typescript
|
||||
interface TransformOptions {
|
||||
width?: number; height?: number;
|
||||
fit?: "scale-down" | "contain" | "cover" | "crop" | "pad";
|
||||
quality?: number; // 1-100
|
||||
format?: "avif" | "webp" | "jpeg" | "png";
|
||||
dpr?: number; // 1-3
|
||||
gravity?: "auto" | "left" | "right" | "top" | "bottom" | "face" | string;
|
||||
sharpen?: number; // 0-10
|
||||
blur?: number; // 1-250
|
||||
rotate?: 90 | 180 | 270;
|
||||
background?: string; // CSS color for pad
|
||||
metadata?: "none" | "copyright" | "keep";
|
||||
brightness?: number; contrast?: number; gamma?: number; // 0-2
|
||||
}
|
||||
```
|
||||
|
||||
### Draw/Watermark
|
||||
|
||||
```typescript
|
||||
await env.IMAGES.input(baseImage)
|
||||
.draw(env.IMAGES.input(watermark).transform({ width: 100 }), { top: 10, left: 10, opacity: 0.8 })
|
||||
.output();
|
||||
```
|
||||
|
||||
## REST API
|
||||
|
||||
### Upload Image
|
||||
|
||||
```bash
|
||||
curl -X POST https://api.cloudflare.com/client/v4/accounts/{account_id}/images/v1 \
|
||||
-H "Authorization: Bearer {token}" -F file=@image.jpg -F metadata='{"key":"value"}'
|
||||
```
|
||||
|
||||
### Other Operations
|
||||
|
||||
```bash
|
||||
GET /accounts/{account_id}/images/v1/{image_id} # Get details
|
||||
DELETE /accounts/{account_id}/images/v1/{image_id} # Delete
|
||||
GET /accounts/{account_id}/images/v1?page=1 # List
|
||||
```
|
||||
|
||||
## URL Transform API
|
||||
|
||||
```
|
||||
https://imagedelivery.net/{hash}/{id}/width=800,height=600,fit=cover,format=avif
|
||||
```
|
||||
|
||||
**Params:** `w=`, `h=`, `fit=`, `q=`, `f=`, `dpr=`, `gravity=`, `sharpen=`, `blur=`, `rotate=`, `background=`, `metadata=`
|
||||
|
||||
## Direct Creator Upload
|
||||
|
||||
```typescript
|
||||
// 1. Get upload URL (backend)
|
||||
const { result } = await fetch(
|
||||
`https://api.cloudflare.com/client/v4/accounts/${accountId}/images/v2/direct_upload`,
|
||||
{ method: 'POST', headers: { 'Authorization': `Bearer ${token}` },
|
||||
body: JSON.stringify({ requireSignedURLs: false }) }
|
||||
).then(r => r.json());
|
||||
|
||||
// 2. Client uploads to result.uploadURL
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
await fetch(result.uploadURL, { method: 'POST', body: formData });
|
||||
```
|
||||
|
||||
## Error Codes
|
||||
|
||||
| Code | Message | Solution |
|
||||
|------|---------|----------|
|
||||
| 5400 | Invalid format | Use JPEG, PNG, GIF, WebP |
|
||||
| 5401 | Too large | Max 100MB |
|
||||
| 5403 | Invalid transform | Check params |
|
||||
| 9413 | Rate limit | Implement backoff |
|
||||
@@ -0,0 +1,211 @@
|
||||
# Configuration
|
||||
|
||||
## Wrangler Integration
|
||||
|
||||
### Workers Binding Setup
|
||||
|
||||
Add to `wrangler.toml`:
|
||||
|
||||
```toml
|
||||
name = "my-image-worker"
|
||||
main = "src/index.ts"
|
||||
compatibility_date = "2024-01-01"
|
||||
|
||||
[images]
|
||||
binding = "IMAGES"
|
||||
```
|
||||
|
||||
Access in Worker:
|
||||
|
||||
```typescript
|
||||
interface Env {
|
||||
IMAGES: ImageBinding;
|
||||
}
|
||||
|
||||
export default {
|
||||
async fetch(request: Request, env: Env): Promise<Response> {
|
||||
return await env.IMAGES
|
||||
.input(imageBuffer)
|
||||
.transform({ width: 800 })
|
||||
.output()
|
||||
.response();
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### Upload via Script
|
||||
|
||||
Wrangler doesn't have built-in Images commands, use REST API:
|
||||
|
||||
```typescript
|
||||
// scripts/upload-image.ts
|
||||
import fs from 'fs';
|
||||
import FormData from 'form-data';
|
||||
|
||||
async function uploadImage(filePath: string) {
|
||||
const accountId = process.env.CLOUDFLARE_ACCOUNT_ID!;
|
||||
const apiToken = process.env.CLOUDFLARE_API_TOKEN!;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', fs.createReadStream(filePath));
|
||||
|
||||
const response = await fetch(
|
||||
`https://api.cloudflare.com/client/v4/accounts/${accountId}/images/v1`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${apiToken}`,
|
||||
},
|
||||
body: formData,
|
||||
}
|
||||
);
|
||||
|
||||
const result = await response.json();
|
||||
console.log('Uploaded:', result);
|
||||
}
|
||||
|
||||
uploadImage('./photo.jpg');
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
|
||||
Store account hash for URL construction:
|
||||
|
||||
```toml
|
||||
[vars]
|
||||
IMAGES_ACCOUNT_HASH = "your-account-hash"
|
||||
ACCOUNT_ID = "your-account-id"
|
||||
```
|
||||
|
||||
Access in Worker:
|
||||
|
||||
```typescript
|
||||
const imageUrl = `https://imagedelivery.net/${env.IMAGES_ACCOUNT_HASH}/${imageId}/public`;
|
||||
```
|
||||
|
||||
## Variants Configuration
|
||||
|
||||
Variants are named presets for transformations.
|
||||
|
||||
### Create Variant (Dashboard)
|
||||
|
||||
1. Navigate to Images → Variants
|
||||
2. Click "Create Variant"
|
||||
3. Set name (e.g., `thumbnail`)
|
||||
4. Configure: `width=200,height=200,fit=cover`
|
||||
|
||||
### Create Variant (API)
|
||||
|
||||
```bash
|
||||
curl -X POST \
|
||||
https://api.cloudflare.com/client/v4/accounts/{account_id}/images/v1/variants \
|
||||
-H "Authorization: Bearer {api_token}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"id": "thumbnail",
|
||||
"options": {
|
||||
"width": 200,
|
||||
"height": 200,
|
||||
"fit": "cover"
|
||||
},
|
||||
"neverRequireSignedURLs": true
|
||||
}'
|
||||
```
|
||||
|
||||
### Use Variant
|
||||
|
||||
```
|
||||
https://imagedelivery.net/{account_hash}/{image_id}/thumbnail
|
||||
```
|
||||
|
||||
### Common Variant Presets
|
||||
|
||||
```json
|
||||
{
|
||||
"thumbnail": {
|
||||
"width": 200,
|
||||
"height": 200,
|
||||
"fit": "cover"
|
||||
},
|
||||
"avatar": {
|
||||
"width": 128,
|
||||
"height": 128,
|
||||
"fit": "cover",
|
||||
"gravity": "face"
|
||||
},
|
||||
"hero": {
|
||||
"width": 1920,
|
||||
"height": 1080,
|
||||
"fit": "cover",
|
||||
"quality": 90
|
||||
},
|
||||
"mobile": {
|
||||
"width": 640,
|
||||
"fit": "scale-down",
|
||||
"quality": 80,
|
||||
"format": "avif"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Authentication
|
||||
|
||||
### API Token (Recommended)
|
||||
|
||||
Generate at: Dashboard → My Profile → API Tokens
|
||||
|
||||
Required permissions:
|
||||
- Account → Cloudflare Images → Edit
|
||||
|
||||
```bash
|
||||
curl -H "Authorization: Bearer {api_token}" \
|
||||
https://api.cloudflare.com/client/v4/accounts/{account_id}/images/v1
|
||||
```
|
||||
|
||||
### API Key (Legacy)
|
||||
|
||||
```bash
|
||||
curl -H "X-Auth-Email: {email}" \
|
||||
-H "X-Auth-Key: {api_key}" \
|
||||
https://api.cloudflare.com/client/v4/accounts/{account_id}/images/v1
|
||||
```
|
||||
|
||||
## Signed URLs
|
||||
|
||||
For private images, enable signed URLs:
|
||||
|
||||
```bash
|
||||
# Upload with signed URLs required
|
||||
curl -X POST \
|
||||
https://api.cloudflare.com/client/v4/accounts/{account_id}/images/v1 \
|
||||
-H "Authorization: Bearer {api_token}" \
|
||||
-F file=@private.jpg \
|
||||
-F requireSignedURLs=true
|
||||
```
|
||||
|
||||
Generate signed URL:
|
||||
|
||||
```typescript
|
||||
import { createHmac } from 'crypto';
|
||||
|
||||
function signUrl(imageId: string, variant: string, expiry: number, key: string): string {
|
||||
const path = `/${imageId}/${variant}`;
|
||||
const toSign = `${path}${expiry}`;
|
||||
const signature = createHmac('sha256', key)
|
||||
.update(toSign)
|
||||
.digest('hex');
|
||||
|
||||
return `https://imagedelivery.net/{hash}${path}?exp=${expiry}&sig=${signature}`;
|
||||
}
|
||||
|
||||
// Sign URL valid for 1 hour
|
||||
const signedUrl = signUrl('image-id', 'public', Date.now() + 3600, env.SIGNING_KEY);
|
||||
```
|
||||
|
||||
## Local Development
|
||||
|
||||
```bash
|
||||
npx wrangler dev --remote
|
||||
```
|
||||
|
||||
Must use `--remote` for Images binding access.
|
||||
@@ -0,0 +1,99 @@
|
||||
# Gotchas & Best Practices
|
||||
|
||||
## Fit Modes
|
||||
|
||||
| Mode | Best For | Behavior |
|
||||
|------|----------|----------|
|
||||
| `cover` | Hero images, thumbnails | Fills space, crops excess |
|
||||
| `contain` | Product images, artwork | Preserves full image, may add padding |
|
||||
| `scale-down` | User uploads | Never enlarges |
|
||||
| `crop` | Precise crops | Uses gravity |
|
||||
| `pad` | Fixed aspect ratio | Adds background |
|
||||
|
||||
## Format Selection
|
||||
|
||||
```typescript
|
||||
format: 'auto' // Recommended - negotiates best format
|
||||
```
|
||||
|
||||
**Support:** AVIF (Chrome 85+, Firefox 93+, Safari 16.4+), WebP (Chrome 23+, Firefox 65+, Safari 14+)
|
||||
|
||||
## Quality Settings
|
||||
|
||||
| Use Case | Quality |
|
||||
|----------|---------|
|
||||
| Thumbnails | 75-80 |
|
||||
| Standard | 85 (default) |
|
||||
| High-quality | 90-95 |
|
||||
|
||||
## Common Errors
|
||||
|
||||
### 5403: "Image transformation failed"
|
||||
- Verify `width`/`height` ≤ 12000
|
||||
- Check `quality` 1-100, `dpr` 1-3
|
||||
- Don't combine incompatible options
|
||||
|
||||
### 9413: "Rate limit exceeded"
|
||||
Implement caching and exponential backoff:
|
||||
```typescript
|
||||
for (let i = 0; i < 3; i++) {
|
||||
try { return await env.IMAGES.input(buffer).transform({...}).output(); }
|
||||
catch { await new Promise(r => setTimeout(r, 2 ** i * 1000)); }
|
||||
}
|
||||
```
|
||||
|
||||
### 5401: "Image too large"
|
||||
Pre-process images before upload (max 100MB, 12000×12000px)
|
||||
|
||||
### 5400: "Invalid image format"
|
||||
Supported: JPEG, PNG, GIF, WebP, AVIF, SVG
|
||||
|
||||
### 401/403: "Unauthorized"
|
||||
Verify API token has `Cloudflare Images → Edit` permission
|
||||
|
||||
## Limits
|
||||
|
||||
| Resource | Limit |
|
||||
|----------|-------|
|
||||
| Max input size | 100MB |
|
||||
| Max dimensions | 12000×12000px |
|
||||
| Quality range | 1-100 |
|
||||
| DPR range | 1-3 |
|
||||
| API rate limit | ~1200 req/min |
|
||||
|
||||
## AVIF Gotchas
|
||||
|
||||
- **Slower encoding**: First request may have higher latency
|
||||
- **Browser detection**:
|
||||
```typescript
|
||||
const format = /image\/avif/.test(request.headers.get('Accept') || '') ? 'avif' : 'webp';
|
||||
```
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
```typescript
|
||||
// ❌ No caching - transforms every request
|
||||
return env.IMAGES.input(buffer).transform({...}).output().response();
|
||||
|
||||
// ❌ cover without both dimensions
|
||||
transform({ width: 800, fit: 'cover' })
|
||||
|
||||
// ✅ Always set both for cover
|
||||
transform({ width: 800, height: 600, fit: 'cover' })
|
||||
|
||||
// ❌ Exposes API token to client
|
||||
// ✅ Use Direct Creator Upload (patterns.md)
|
||||
```
|
||||
|
||||
## Debugging
|
||||
|
||||
```typescript
|
||||
// Check response headers
|
||||
console.log('Content-Type:', response.headers.get('Content-Type'));
|
||||
|
||||
// Test with curl
|
||||
// curl -I "https://imagedelivery.net/{hash}/{id}/width=800,format=avif"
|
||||
|
||||
// Monitor logs
|
||||
// npx wrangler tail
|
||||
```
|
||||
115
.agents/skills/cloudflare-deploy/references/images/patterns.md
Normal file
115
.agents/skills/cloudflare-deploy/references/images/patterns.md
Normal file
@@ -0,0 +1,115 @@
|
||||
# Common Patterns
|
||||
|
||||
## URL Transform Options
|
||||
|
||||
```
|
||||
width=<PX> height=<PX> fit=scale-down|contain|cover|crop|pad
|
||||
quality=85 format=auto|webp|avif|jpeg|png dpr=2
|
||||
gravity=auto|face|left|right|top|bottom sharpen=2 blur=10
|
||||
rotate=90|180|270 background=white metadata=none|copyright|keep
|
||||
```
|
||||
|
||||
## Responsive Images (srcset)
|
||||
|
||||
```html
|
||||
<img src="https://imagedelivery.net/{hash}/{id}/width=800"
|
||||
srcset=".../{id}/width=400 400w, .../{id}/width=800 800w, .../{id}/width=1200 1200w"
|
||||
sizes="(max-width: 600px) 400px, 800px" />
|
||||
```
|
||||
|
||||
## Format Negotiation
|
||||
|
||||
```typescript
|
||||
async fetch(request: Request, env: Env): Promise<Response> {
|
||||
const accept = request.headers.get('Accept') || '';
|
||||
const format = /image\/avif/.test(accept) ? 'avif' : /image\/webp/.test(accept) ? 'webp' : 'jpeg';
|
||||
return env.IMAGES.input(buffer).transform({ format, quality: 85 }).output().response();
|
||||
}
|
||||
```
|
||||
|
||||
## Direct Creator Upload
|
||||
|
||||
```typescript
|
||||
// Backend: Generate upload URL
|
||||
const response = await fetch(
|
||||
`https://api.cloudflare.com/client/v4/accounts/${env.ACCOUNT_ID}/images/v2/direct_upload`,
|
||||
{ method: 'POST', headers: { 'Authorization': `Bearer ${env.API_TOKEN}` },
|
||||
body: JSON.stringify({ requireSignedURLs: false, metadata: { userId } }) }
|
||||
);
|
||||
|
||||
// Frontend: Upload to returned uploadURL
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
await fetch(result.uploadURL, { method: 'POST', body: formData });
|
||||
// Use: https://imagedelivery.net/{hash}/${result.id}/public
|
||||
```
|
||||
|
||||
## Transform & Store to R2
|
||||
|
||||
```typescript
|
||||
async fetch(request: Request, env: Env): Promise<Response> {
|
||||
const file = (await request.formData()).get('image') as File;
|
||||
const transformed = await env.IMAGES
|
||||
.input(await file.arrayBuffer())
|
||||
.transform({ width: 800, format: 'avif', quality: 80 })
|
||||
.output();
|
||||
await env.R2.put(`images/${Date.now()}.avif`, transformed.response().body);
|
||||
return Response.json({ success: true });
|
||||
}
|
||||
```
|
||||
|
||||
## Watermarking
|
||||
|
||||
```typescript
|
||||
const watermark = await env.ASSETS.fetch(new URL('/watermark.png', request.url));
|
||||
const result = await env.IMAGES
|
||||
.input(await image.arrayBuffer())
|
||||
.draw(env.IMAGES.input(watermark.body).transform({ width: 100 }), { bottom: 20, right: 20, opacity: 0.7 })
|
||||
.transform({ format: 'avif' })
|
||||
.output();
|
||||
return result.response();
|
||||
```
|
||||
|
||||
## Device-Based Transforms
|
||||
|
||||
```typescript
|
||||
const ua = request.headers.get('User-Agent') || '';
|
||||
const isMobile = /Mobile|Android|iPhone/i.test(ua);
|
||||
return env.IMAGES.input(buffer)
|
||||
.transform({ width: isMobile ? 400 : 1200, quality: isMobile ? 75 : 85, format: 'avif' })
|
||||
.output().response();
|
||||
```
|
||||
|
||||
## Caching Strategy
|
||||
|
||||
```typescript
|
||||
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
|
||||
const cache = caches.default;
|
||||
let response = await cache.match(request);
|
||||
if (!response) {
|
||||
response = await env.IMAGES.input(buffer).transform({ width: 800, format: 'avif' }).output().response();
|
||||
response = new Response(response.body, { headers: { ...response.headers, 'Cache-Control': 'public, max-age=86400' } });
|
||||
ctx.waitUntil(cache.put(request, response.clone()));
|
||||
}
|
||||
return response;
|
||||
}
|
||||
```
|
||||
|
||||
## Batch Processing
|
||||
|
||||
```typescript
|
||||
const results = await Promise.all(images.map(buffer =>
|
||||
env.IMAGES.input(buffer).transform({ width: 800, fit: 'cover', format: 'avif' }).output()
|
||||
));
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
```typescript
|
||||
try {
|
||||
return (await env.IMAGES.input(buffer).transform({ width: 800 }).output()).response();
|
||||
} catch (error) {
|
||||
console.error('Transform failed:', error);
|
||||
return new Response('Image processing failed', { status: 500 });
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user