Cloudflare Workers Samples

Cloudflare Workers edge computing examples including API proxies, image processing, and edge-first applications

💻 API Proxy Worker javascript

🟢 simple ⭐⭐

Edge API proxy with caching, rate limiting, and request transformation

⏱️ 15 min 🏷️ cloudflare, workers, api, proxy, caching
Prerequisites: Cloudflare account, Workers enabled, Basic JavaScript knowledge
// Cloudflare Workers API Proxy
// src/index.js - Main worker file

export default {
  async fetch(request, env, ctx) {
    const url = new URL(request.url);
    const pathname = url.pathname;

    // Handle different routes
    if (pathname === '/') {
      return new Response('Hello from Cloudflare Workers API Proxy!', {
        headers: {
          'Content-Type': 'text/plain',
          'Cache-Control': 'public, max-age=86400'
        }
      });
    }

    if (pathname === '/health') {
      return Response.json({
        status: 'healthy',
        timestamp: new Date().toISOString(),
        region: request.cf.colo,
        ip: request.cf.colo
      });
    }

    // API proxy functionality
    if (pathname.startsWith('/api/')) {
      return handleAPIProxy(request, env);
    }

    // Default response
    return new Response('Not Found', { status: 404 });
  }
};

async function handleAPIProxy(request, env) {
  const url = new URL(request.url);
  const targetPath = url.pathname.replace('/api', '');
  const targetURL = `${env.API_BASE_URL}${targetPath}${url.search}`;

  // Rate limiting check
  const clientIP = request.headers.get('CF-Connecting-IP') || request.headers.get('X-Forwarded-For');
  const rateLimitKey = `rate_limit:${clientIP}`;

  const { success: rateLimitResult } = await env.RATE_LIMITER.limit({
    key: rateLimitKey,
    limit: 100, // 100 requests per minute
    ttl: 60,   // per minute
  });

  if (!rateLimitResult) {
    return new Response('Rate limit exceeded', {
      status: 429,
      headers: {
        'Retry-After': '60',
        'X-RateLimit-Limit': '100',
        'X-RateLimit-Remaining': '0',
        'X-RateLimit-Reset': Math.ceil(Date.now() / 1000) + 60
      }
    });
  }

  try {
    // Check cache first
    const cacheKey = new Request(targetURL, {
      method: request.method,
      headers: request.headers
    });

    const cache = caches.default;
    let response = await cache.match(cacheKey);

    if (!response) {
      // Cache miss - make request to target API
      const init = {
        method: request.method,
        headers: {},
        body: request.body
      };

      // Copy headers (except CF-specific headers)
      request.headers.forEach((value, key) => {
        if (!key.startsWith('CF-')) {
          init.headers[key] = value;
        }
      });

      // Add custom headers
      init.headers['X-Forwarded-For'] = clientIP;
      init.headers['X-Proxy-By'] = 'Cloudflare Workers';

      const targetResponse = await fetch(targetURL, init);

      // Transform response
      const responseBody = await targetResponse.text();
      const transformedBody = transformResponse(responseBody, targetURL);

      response = new Response(transformedBody, {
        status: targetResponse.status,
        statusText: targetResponse.statusText,
        headers: targetResponse.headers
      });

      // Add custom headers to response
      response.headers.set('X-Cache-Status', 'MISS');
      response.headers.set('X-Proxy-Timestamp', new Date().toISOString());
      response.headers.set('X-Edge-Location', request.cf.colo);

      // Cache successful responses
      if (targetResponse.ok) {
        ctx.waitUntil(cache.put(cacheKey, response.clone()));
      }
    } else {
      // Cache hit
      response = new Response(response.body, response);
      response.headers.set('X-Cache-Status', 'HIT');
    }

    // Add rate limit headers
    const remaining = await env.RATE_LIMITER.remaining({ key: rateLimitKey });
    response.headers.set('X-RateLimit-Limit', '100');
    response.headers.set('X-RateLimit-Remaining', remaining.toString());
    response.headers.set('X-RateLimit-Reset', Math.ceil(Date.now() / 1000) + 60);

    return response;

  } catch (error) {
    console.error('Proxy error:', error);

    return Response.json({
      error: 'Proxy request failed',
      message: error.message,
      timestamp: new Date().toISOString()
    }, { status: 502 });
  }
}

function transformResponse(responseBody, targetURL) {
  try {
    // Try to parse as JSON and transform
    const data = JSON.parse(responseBody);

    // Add proxy metadata
    if (Array.isArray(data)) {
      return JSON.stringify({
        data: data,
        meta: {
          proxied: true,
          source: targetURL,
          timestamp: new Date().toISOString(),
          total: data.length
        }
      });
    } else if (typeof data === 'object') {
      return JSON.stringify({
        ...data,
        meta: {
          proxied: true,
          source: targetURL,
          timestamp: new Date().toISOString()
        }
      });
    }
  } catch (e) {
    // Not JSON, return as-is
  }

  return responseBody;
}

// wrangler.toml - Configuration file
/*
name = "api-proxy-worker"
main = "src/index.js"
compatibility_date = "2023-10-30"

# Environment variables
[env.production]
API_BASE_URL = "https://api.example.com"

# KV namespace for rate limiting
[[kv_namespaces]]
binding = "RATE_LIMITER"
id = "your-kv-namespace-id"
preview_id = "your-preview-kv-namespace-id"
*/

💻 Image Processing Worker typescript

🟡 intermediate ⭐⭐⭐

On-the-fly image resizing, optimization, and transformation at the edge

⏱️ 30 min 🏷️ cloudflare, workers, image, processing, transformation
Prerequisites: Cloudflare account, Workers and R2 enabled, TypeScript knowledge
// Cloudflare Workers Image Processing
// src/index.ts - TypeScript implementation with advanced image processing

import { ImageResponse } from 'cloudflare-workers-experimental';

interface ImageOptions {
  width?: number;
  height?: number;
  quality?: number;
  format?: 'webp' | 'jpeg' | 'png' | 'avif';
  fit?: 'cover' | 'contain' | 'fill' | 'inside' | 'outside';
  gravity?: 'auto' | 'center' | 'top' | 'bottom' | 'left' | 'right';
  blur?: number;
  sharpen?: number;
  brightness?: number;
  contrast?: number;
  saturation?: number;
}

export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
    const url = new URL(request.url);
    const pathname = url.pathname;

    // Handle image processing requests
    if (pathname.startsWith('/process/')) {
      return handleImageProcessing(request, env);
    }

    // Handle direct image uploads
    if (pathname === '/upload' && request.method === 'POST') {
      return handleImageUpload(request, env);
    }

    // Handle health check
    if (pathname === '/health') {
      return Response.json({
        status: 'healthy',
        timestamp: new Date().toISOString(),
        features: {
          imageProcessing: true,
          transformations: ['resize', 'optimize', 'format-convert'],
          supportedFormats: ['webp', 'jpeg', 'png', 'avif']
        }
      });
    }

    // Serve processing UI
    if (pathname === '/') {
      return new Response(getProcessingUI(), {
        headers: { 'Content-Type': 'text/html' }
      });
    }

    return new Response('Not Found', { status: 404 });
  }
};

async function handleImageProcessing(request: Request, env: Env): Promise<Response> {
  try {
    const url = new URL(request.url);
    const imagePath = url.pathname.replace('/process/', '');
    const imageUrl = decodeURIComponent(imagePath);

    // Parse query parameters
    const options: ImageOptions = {
      width: parseInt(url.searchParams.get('width') || '0'),
      height: parseInt(url.searchParams.get('height') || '0'),
      quality: parseInt(url.searchParams.get('quality') || '80'),
      format: url.searchParams.get('format') as any || 'auto',
      fit: url.searchParams.get('fit') as any || 'cover',
      gravity: url.searchParams.get('gravity') as any || 'auto',
      blur: parseFloat(url.searchParams.get('blur') || '0'),
      sharpen: parseFloat(url.searchParams.get('sharpen') || '0'),
      brightness: parseFloat(url.searchParams.get('brightness') || '1'),
      contrast: parseFloat(url.searchParams.get('contrast') || '1'),
      saturation: parseFloat(url.searchParams.get('saturation') || '1')
    };

    // Validate parameters
    if (options.width && (options.width < 1 || options.width > 4096)) {
      return Response.json({ error: 'Width must be between 1 and 4096' }, { status: 400 });
    }
    if (options.height && (options.height < 1 || options.height > 4096)) {
      return Response.json({ error: 'Height must be between 1 and 4096' }, { status: 400 });
    }
    if (options.quality && (options.quality < 1 || options.quality > 100)) {
      return Response.json({ error: 'Quality must be between 1 and 100' }, { status: 400 });
    }

    // Fetch original image
    let imageResponse: Response;

    if (imageUrl.startsWith('http://') || imageUrl.startsWith('https://')) {
      // Fetch from external URL
      imageResponse = await fetch(imageUrl);
    } else {
      // Fetch from R2 storage
      const objectKey = imageUrl.startsWith('/') ? imageUrl.slice(1) : imageUrl;
      imageResponse = await env.IMAGE_BUCKET.get(objectKey);

      if (!imageResponse) {
        return Response.json({ error: 'Image not found' }, { status: 404 });
      }
    }

    if (!imageResponse.ok) {
      return Response.json({ error: 'Failed to fetch image' }, { status: 502 });
    }

    const originalImage = await imageResponse.arrayBuffer();

    // Detect original format
    const originalFormat = detectImageFormat(originalImage);

    // Determine target format
    const targetFormat = options.format === 'auto' ?
      getOptimalFormat(request.headers.get('Accept'), originalFormat) :
      options.format;

    // Process image using ImageMagick (if available) or simple resizing
    const processedImage = await processImage(originalImage, {
      ...options,
      format: targetFormat,
      originalFormat
    });

    // Generate cache-friendly filename
    const processedUrl = generateProcessedUrl(imageUrl, options);

    // Store processed image in R2 for future use
    ctx.waitUntil(env.IMAGE_BUCKET.put(processedUrl, processedImage, {
      httpMetadata: {
        contentType: `image/${targetFormat}`,
        cacheControl: 'public, max-age=31536000'
      }
    }));

    // Return processed image
    return new Response(processedImage, {
      headers: {
        'Content-Type': `image/${targetFormat}`,
        'Cache-Control': 'public, max-age=31536000',
        'X-Processed-By': 'Cloudflare Workers',
        'X-Original-Format': originalFormat,
        'X-Target-Format': targetFormat,
        'X-Processing-Time': Date.now().toString()
      }
    });

  } catch (error) {
    console.error('Image processing error:', error);
    return Response.json({
      error: 'Image processing failed',
      message: error.message
    }, { status: 500 });
  }
}

async function handleImageUpload(request: Request, env: Env): Promise<Response> {
  try {
    const formData = await request.formData();
    const file = formData.get('image') as File;

    if (!file) {
      return Response.json({ error: 'No image file provided' }, { status: 400 });
    }

    // Validate file type
    const allowedTypes = ['image/jpeg', 'image/png', 'image/webp', 'image/gif'];
    if (!allowedTypes.includes(file.type)) {
      return Response.json({ error: 'Invalid file type' }, { status: 400 });
    }

    // Validate file size (10MB limit)
    if (file.size > 10 * 1024 * 1024) {
      return Response.json({ error: 'File too large (max 10MB)' }, { status: 400 });
    }

    // Generate unique filename
    const fileExtension = file.name.split('.').pop();
    const uniqueId = crypto.randomUUID();
    const filename = `${uniqueId}.${fileExtension}`;

    // Store in R2
    await env.IMAGE_BUCKET.put(filename, file.stream(), {
      httpMetadata: {
        contentType: file.type,
        contentDisposition: `inline; filename="${file.name}"`,
        cacheControl: 'public, max-age=31536000'
      }
    });

    return Response.json({
      success: true,
      filename,
      originalName: file.name,
      size: file.size,
      type: file.type,
      processingUrl: `https://${request.headers.get('host')}/process/${filename}`
    });

  } catch (error) {
    console.error('Upload error:', error);
    return Response.json({
      error: 'Upload failed',
      message: error.message
    }, { status: 500 });
  }
}

function detectImageFormat(imageBuffer: ArrayBuffer): string {
  const view = new Uint8Array(imageBuffer);

  // Check magic numbers
  if (view[0] === 0xFF && view[1] === 0xD8) return 'jpeg';
  if (view[0] === 0x89 && view[1] === 0x50) return 'png';
  if (view[0] === 0x52 && view[1] === 0x49) return 'webp';
  if (view[0] === 0x47 && view[1] === 0x49) return 'gif';

  return 'unknown';
}

function getOptimalFormat(acceptHeader: string | null, originalFormat: string): string {
  if (!acceptHeader) return originalFormat;

  // Priority order based on browser support
  if (acceptHeader.includes('image/avif')) return 'avif';
  if (acceptHeader.includes('image/webp')) return 'webp';

  return originalFormat;
}

async function processImage(
  imageBuffer: ArrayBuffer,
  options: ImageOptions & { originalFormat: string }
): Promise<ArrayBuffer> {
  // Simplified image processing
  // In production, you might use @cloudflare/wrangler-image-tools or external API

  const { Image } = await import('image-js');

  try {
    const image = await Image.load(imageBuffer);

    // Apply transformations
    if (options.width || options.height) {
      const width = options.width || image.width;
      const height = options.height || image.height;

      // Resize with aspect ratio preservation
      const aspectRatio = image.width / image.height;
      const targetAspectRatio = width / height;

      let cropWidth = image.width;
      let cropHeight = image.height;

      if (options.fit === 'cover') {
        if (targetAspectRatio > aspectRatio) {
          cropWidth = image.height * targetAspectRatio;
        } else {
          cropHeight = image.width / targetAspectRatio;
        }
      }

      // Crop and resize
      const cropX = (image.width - cropWidth) / 2;
      const cropY = (image.height - cropHeight) / 2;

      image.crop({
        x: cropX,
        y: cropY,
        width: cropWidth,
        height: cropHeight
      });

      image.resize({ width, height });
    }

    // Apply filters
    if (options.brightness && options.brightness !== 1) {
      image.adjustBrightness(options.brightness);
    }

    if (options.contrast && options.contrast !== 1) {
      image.adjustContrast(options.contrast);
    }

    if (options.saturation && options.saturation !== 1) {
      image.adjustSaturation(options.saturation);
    }

    // Convert format
    const format = options.format || options.originalFormat;

    let processedBuffer: ArrayBuffer;
    switch (format) {
      case 'jpeg':
        processedBuffer = await image.encodeJPEG({ quality: options.quality });
        break;
      case 'png':
        processedBuffer = await image.encodePNG();
        break;
      case 'webp':
        processedBuffer = await image.encodeWebP({ quality: options.quality });
        break;
      case 'avif':
        processedBuffer = await image.encodeAVIF({ quality: options.quality });
        break;
      default:
        processedBuffer = imageBuffer;
    }

    return processedBuffer;

  } catch (error) {
    console.error('Image processing failed:', error);
    return imageBuffer; // Return original if processing fails
  }
}

function generateProcessedUrl(imageUrl: string, options: ImageOptions): string {
  const params = new URLSearchParams();

  Object.entries(options).forEach(([key, value]) => {
    if (value !== undefined && value !== null && value !== '' && value !== 0) {
      params.set(key, value.toString());
    }
  });

  const queryString = params.toString();
  const baseName = imageUrl.includes('.') ? imageUrl.split('.')[0] : imageUrl;
  const extension = imageUrl.includes('.') ? imageUrl.split('.').pop() : 'jpg';

  return queryString ? `${baseName}?${queryString}.${extension}` : imageUrl;
}

function getProcessingUI(): string {
  return `
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Cloudflare Workers Image Processor</title>
    <style>
        body { font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; }
        .container { background: #f5f5f5; padding: 20px; border-radius: 8px; }
        .form-group { margin-bottom: 15px; }
        label { display: block; margin-bottom: 5px; font-weight: bold; }
        input, select { width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; }
        button { background: #007cba; color: white; padding: 10px 20px; border: none; border-radius: 4px; cursor: pointer; }
        button:hover { background: #005a87; }
        .preview { margin-top: 20px; text-align: center; }
        .preview img { max-width: 100%; height: auto; border: 1px solid #ddd; }
    </style>
</head>
<body>
    <div class="container">
        <h1>🖼️ Cloudflare Workers Image Processor</h1>

        <h2>Upload Image</h2>
        <form id="uploadForm">
            <div class="form-group">
                <label for="imageFile">Choose image:</label>
                <input type="file" id="imageFile" accept="image/*" required>
            </div>
            <button type="submit">Upload</button>
        </form>

        <div id="result"></div>

        <h2>Process Existing Image</h2>
        <form id="processForm">
            <div class="form-group">
                <label for="imageUrl">Image URL:</label>
                <input type="url" id="imageUrl" placeholder="https://example.com/image.jpg" required>
            </div>

            <div class="form-group">
                <label for="width">Width:</label>
                <input type="number" id="width" min="1" max="4096" placeholder="800">
            </div>

            <div class="form-group">
                <label for="height">Height:</label>
                <input type="number" id="height" min="1" max="4096" placeholder="600">
            </div>

            <div class="form-group">
                <label for="format">Format:</label>
                <select id="format">
                    <option value="auto">Auto</option>
                    <option value="webp">WebP</option>
                    <option value="avif">AVIF</option>
                    <option value="jpeg">JPEG</option>
                    <option value="png">PNG</option>
                </select>
            </div>

            <div class="form-group">
                <label for="quality">Quality:</label>
                <input type="range" id="quality" min="1" max="100" value="80">
                <span id="qualityValue">80</span>
            </div>

            <button type="submit">Process Image</button>
        </form>

        <div id="processedResult" class="preview"></div>
    </div>

    <script>
        // Quality slider
        const qualitySlider = document.getElementById('quality');
        const qualityValue = document.getElementById('qualityValue');
        qualitySlider.addEventListener('input', () => {
            qualityValue.textContent = qualitySlider.value;
        });

        // Upload form
        document.getElementById('uploadForm').addEventListener('submit', async (e) => {
            e.preventDefault();
            const formData = new FormData();
            formData.append('image', document.getElementById('imageFile').files[0]);

            try {
                const response = await fetch('/upload', {
                    method: 'POST',
                    body: formData
                });

                const result = await response.json();

                if (result.success) {
                    document.getElementById('result').innerHTML =
                        `<div style="color: green;">
                            <strong>Upload successful!</strong><br>
                            Filename: ${result.filename}<br>
                            <a href="${result.processingUrl}">Process this image</a>
                        </div>`;
                } else {
                    document.getElementById('result').innerHTML =
                        `<div style="color: red;">Error: ${result.error}</div>`;
                }
            } catch (error) {
                document.getElementById('result').innerHTML =
                    `<div style="color: red;">Error: ${error.message}</div>`;
            }
        });

        // Process form
        document.getElementById('processForm').addEventListener('submit', async (e) => {
            e.preventDefault();

            const imageUrl = document.getElementById('imageUrl').value;
            const width = document.getElementById('width').value;
            const height = document.getElementById('height').value;
            const format = document.getElementById('format').value;
            const quality = document.getElementById('quality').value;

            const processedUrl = new URL('/process/' + encodeURIComponent(imageUrl), window.location.origin);

            if (width) processedUrl.searchParams.set('width', width);
            if (height) processedUrl.searchParams.set('height', height);
            if (format !== 'auto') processedUrl.searchParams.set('format', format);
            processedUrl.searchParams.set('quality', quality);

            document.getElementById('processedResult').innerHTML =
                `
                <h3>Processed Image:</h3>
                <p>URL: <a href="${processedUrl}" target="_blank">${processedUrl}</a></p>
                <img src="${processedUrl}" alt="Processed image" style="max-width: 100%; height: auto;">
                `;
        });
    </script>
</body>
</html>
`;
}

// wrangler.toml
/*
name = "image-processor"
main = "src/index.ts"
compatibility_date = "2023-10-30"

# Bindings
[[r2_buckets]]
binding = "IMAGE_BUCKET"
bucket_name = "your-image-bucket"

# Dependencies
[build]
command = "npm install && npm run build"

[vars]
ENVIRONMENT = "production"
*/

💻 WebSocket Server Worker javascript

🟡 intermediate ⭐⭐⭐⭐

Real-time WebSocket server with authentication and room management

⏱️ 35 min 🏷️ cloudflare, workers, websocket, real-time, durable-objects
Prerequisites: Cloudflare account, Workers and Durable Objects enabled, WebSocket knowledge
// Cloudflare Workers WebSocket Server
// src/index.js - Real-time WebSocket implementation with room management

// Durable Object for room management
export class ChatRoom {
  constructor(state, env) {
    this.state = state;
    this.env = env;
    this.sessions = new Map();
    this.roomName = state.id;
    this.connectedClients = 0;

    // Load persisted data if exists
    this.loadPersistedData();
  }

  // Load persisted room data
  async loadPersistedData() {
    try {
      const data = await this.state.storage.get('roomData');
      if (data) {
        this.roomInfo = data;
        console.log(`Loaded persisted data for room ${this.roomName}`);
      } else {
        this.roomInfo = {
          name: this.roomName,
          createdAt: Date.now(),
          messageCount: 0,
          maxClients: 10
        };
      }
    } catch (error) {
      console.error('Error loading persisted data:', error);
      this.roomInfo = {
        name: this.roomName,
        createdAt: Date.now(),
        messageCount: 0,
        maxClients: 10
      };
    }
  }

  // Persist room data
  async persistData() {
    try {
      await this.state.storage.put('roomData', this.roomInfo);
      console.log(`Persisted data for room ${this.roomName}`);
    } catch (error) {
      console.error('Error persisting data:', error);
    }
  }

  async fetch(request) {
    const url = new URL(request.url);
    const pathname = url.pathname;

    if (pathname.startsWith('/socket')) {
      return this.handleWebSocket(request);
    }

    // Handle room management API
    if (pathname === '/info') {
      return new Response(JSON.stringify({
        room: this.roomInfo,
        connectedClients: this.connectedClients,
        capacity: this.roomInfo.maxClients - this.connectedClients
      }), {
        headers: { 'Content-Type': 'application/json' }
      });
    }

    return new Response('Not Found', { status: 404 });
  }

  async handleWebSocket(request) {
    // Check room capacity
    if (this.connectedClients >= this.roomInfo.maxClients) {
      return new Response('Room is full', { status: 429 });
    }

    // Upgrade WebSocket connection
    const pair = new WebSocketPair();
    const [client, server] = pair;

    await this.handleSession(server, request);

    return new Response(null, {
      status: 101,
      webSocket: client
    });
  }

  async handleSession(websocket, request) {
    websocket.accept();

    // Generate session ID
    const sessionId = crypto.randomUUID();

    // Extract client info from request headers
    const userAgent = request.headers.get('User-Agent') || 'Unknown';
    const ip = request.headers.get('CF-Connecting-IP') || 'Unknown';
    const country = request.cf?.country || 'Unknown';

    // Create session object
    const session = {
      id: sessionId,
      websocket,
      ip,
      userAgent,
      country,
      joinedAt: Date.now(),
      lastPing: Date.now(),
      nickname: `User-${sessionId.substring(0, 8)}`,
      isAuthenticated: false,
      room: this.roomName
    };

    this.sessions.set(sessionId, session);
    this.connectedClients++;

    // Send welcome message
    this.sendToSession(sessionId, {
      type: 'welcome',
      data: {
        sessionId,
        roomName: this.roomName,
        connectedClients: this.connectedClients,
        message: 'Connected to chat room'
      }
    });

    // Notify other clients about new user
    this.broadcast({
      type: 'userJoined',
      data: {
        user: {
          id: sessionId,
          nickname: session.nickname,
          country
        },
        totalClients: this.connectedClients
      }
    }, sessionId);

    // Handle WebSocket messages
    websocket.addEventListener('message', (event) => {
      this.handleMessage(sessionId, event.data);
    });

    // Handle WebSocket close
    websocket.addEventListener('close', () => {
      this.handleDisconnect(sessionId);
    });

    // Setup ping/pong for connection health
    const pingInterval = setInterval(() => {
      if (Date.now() - session.lastPing > 30000) { // 30 seconds timeout
        this.handleDisconnect(sessionId);
        clearInterval(pingInterval);
      } else {
        websocket.send(JSON.stringify({ type: 'ping' }));
      }
    }, 15000);

    // Handle pong responses
    websocket.addEventListener('message', (event) => {
      try {
        const message = JSON.parse(event.data);
        if (message.type === 'pong') {
          session.lastPing = Date.now();
        }
      } catch (e) {
        // Ignore parsing errors
      }
    });

    // Store ping interval ID for cleanup
    session.pingInterval = pingInterval;

    console.log(`Client ${sessionId} connected to room ${this.roomName}`);
  }

  handleMessage(sessionId, data) {
    const session = this.sessions.get(sessionId);
    if (!session) return;

    try {
      const message = JSON.parse(data);

      switch (message.type) {
        case 'ping':
          session.lastPing = Date.now();
          break;

        case 'authenticate':
          this.handleAuthentication(sessionId, message.data);
          break;

        case 'setNickname':
          this.handleNicknameChange(sessionId, message.data);
          break;

        case 'chatMessage':
          this.handleChatMessage(sessionId, message.data);
          break;

        case 'privateMessage':
          this.handlePrivateMessage(sessionId, message.data);
          break;

        case 'typing':
          this.handleTypingIndicator(sessionId, message.data);
          break;

        default:
          console.warn(`Unknown message type: ${message.type}`);
      }
    } catch (error) {
      console.error('Error parsing message:', error);
      this.sendToSession(sessionId, {
        type: 'error',
        data: { message: 'Invalid message format' }
      });
    }
  }

  handleAuthentication(sessionId, data) {
    const session = this.sessions.get(sessionId);
    if (!session) return;

    // Simple token-based authentication (in production, use proper auth)
    const token = data.token;

    if (token && token.length > 10) {
      session.isAuthenticated = true;
      session.userType = 'authenticated';

      this.sendToSession(sessionId, {
        type: 'authSuccess',
        data: { userType: 'authenticated' }
      });

      this.broadcast({
        type: 'userAuthenticated',
        data: {
          user: {
            id: sessionId,
            nickname: session.nickname
          }
        }
      });
    }
  }

  handleNicknameChange(sessionId, data) {
    const session = this.sessions.get(sessionId);
    if (!session) return;

    const newNickname = data.nickname?.trim();

    if (newNickname && newNickname.length > 0 && newNickname.length <= 20) {
      const oldNickname = session.nickname;
      session.nickname = newNickname;

      this.sendToSession(sessionId, {
        type: 'nicknameChanged',
        data: { newNickname }
      });

      this.broadcast({
        type: 'userNicknameChanged',
        data: {
          userId: sessionId,
          oldNickname,
          newNickname
        }
      });
    }
  }

  handleChatMessage(sessionId, data) {
    const session = this.sessions.get(sessionId);
    if (!session) return;

    const message = data.message?.trim();

    if (!message || message.length > 500) {
      this.sendToSession(sessionId, {
        type: 'error',
        data: { message: 'Message must be 1-500 characters' }
      });
      return;
    }

    const chatMessage = {
      id: crypto.randomUUID(),
      sessionId,
      nickname: session.nickname,
      message,
      timestamp: Date.now(),
      type: session.isAuthenticated ? 'authenticated' : 'guest'
    };

    // Broadcast to all clients in the room
    this.broadcast({
      type: 'chatMessage',
      data: chatMessage
    });

    // Update message count
    this.roomInfo.messageCount++;

    // Persist every 10 messages
    if (this.roomInfo.messageCount % 10 === 0) {
      this.persistData();
    }

    console.log(`Chat message in ${this.roomName}: ${session.nickname}: ${message}`);
  }

  handlePrivateMessage(sessionId, data) {
    const session = this.sessions.get(sessionId);
    if (!session) return;

    const targetSessionId = data.targetSessionId;
    const message = data.message?.trim();

    if (!targetSessionId || !message) {
      this.sendToSession(sessionId, {
        type: 'error',
        data: { message: 'Invalid private message format' }
      });
      return;
    }

    const targetSession = this.sessions.get(targetSessionId);
    if (!targetSession) {
      this.sendToSession(sessionId, {
        type: 'error',
        data: { message: 'Target user not found' }
      });
      return;
    }

    const privateMessage = {
      id: crypto.randomUUID(),
      fromSessionId: sessionId,
      fromNickname: session.nickname,
      toSessionId: targetSessionId,
      message,
      timestamp: Date.now(),
      type: 'private'
    };

    // Send to target user
    this.sendToSession(targetSessionId, {
      type: 'privateMessage',
      data: privateMessage
    });

    // Send confirmation to sender
    this.sendToSession(sessionId, {
      type: 'privateMessageSent',
      data: {
        ...privateMessage,
        deliveryStatus: 'sent'
      }
    });
  }

  handleTypingIndicator(sessionId, data) {
    const session = this.sessions.get(sessionId);
    if (!session) return;

    const isTyping = data.isTyping;

    this.broadcast({
      type: 'userTyping',
      data: {
        userId: sessionId,
        nickname: session.nickname,
        isTyping
      }
    }, sessionId);
  }

  handleDisconnect(sessionId) {
    const session = this.sessions.get(sessionId);
    if (!session) return;

    // Clear ping interval
    if (session.pingInterval) {
      clearInterval(session.pingInterval);
    }

    // Remove session
    this.sessions.delete(sessionId);
    this.connectedClients--;

    // Notify other clients
    this.broadcast({
      type: 'userLeft',
      data: {
        user: {
          id: sessionId,
          nickname: session.nickname
        },
        totalClients: this.connectedClients
      }
    });

    console.log(`Client ${sessionId} disconnected from room ${this.roomName}`);

    // Persist data when room becomes empty
    if (this.connectedClients === 0) {
      this.persistData();
    }
  }

  sendToSession(sessionId, message) {
    const session = this.sessions.get(sessionId);
    if (!session || !session.websocket) return;

    try {
      session.websocket.send(JSON.stringify(message));
    } catch (error) {
      console.error(`Error sending message to ${sessionId}:`, error);
      this.handleDisconnect(sessionId);
    }
  }

  broadcast(message, excludeSessionId = null) {
    for (const [sessionId, session] of this.sessions) {
      if (sessionId !== excludeSessionId) {
        this.sendToSession(sessionId, message);
      }
    }
  }
}

// Main worker entry point
export default {
  async fetch(request, env, ctx) {
    const url = new URL(request.url);
    const pathname = url.pathname;

    // Handle WebSocket connections to rooms
    if (pathname.startsWith('/room/')) {
      const roomId = pathname.replace('/room/', '');

      // Get room Durable Object
      const room = env.CHAT_ROOMS.get(env.CHAT_ROOMS.idFromName(roomId));

      return room.fetch(request);
    }

    // Handle room creation
    if (pathname === '/create-room' && request.method === 'POST') {
      return handleCreateRoom(request, env);
    }

    // Handle room listing
    if (pathname === '/list-rooms') {
      return handleListRooms(env);
    }

    // Serve WebSocket test page
    if (pathname === '/' || pathname === '/test.html') {
      return new Response(getWebSocketTestPage(), {
        headers: { 'Content-Type': 'text/html' }
      });
    }

    return new Response('Not Found', { status: 404 });
  }
});

async function handleCreateRoom(request, env) {
  try {
    const { roomName, maxClients = 10, isPrivate = false } = await request.json();

    if (!roomName || roomName.trim().length === 0) {
      return new Response(JSON.stringify({ error: 'Room name is required' }), {
        status: 400,
        headers: { 'Content-Type': 'application/json' }
      });
    }

    // Validate room name
    if (roomName.length > 50 || !/^[a-zA-Z0-9_-]+$/.test(roomName)) {
      return new Response(JSON.stringify({
        error: 'Room name must be 1-50 characters and contain only letters, numbers, underscores, and hyphens'
      }), {
        status: 400,
        headers: { 'Content-Type': 'application/json' }
      });
    }

    // Create room
    const roomId = env.CHAT_ROOMS.idFromName(roomName);
    const room = env.CHAT_ROOMS.get(roomId);

    // Store room metadata
    await env.ROOM_METADATA.put(roomName, JSON.stringify({
      name: roomName,
      maxClients,
      isPrivate,
      createdAt: Date.now(),
      createdBy: request.headers.get('CF-Connecting-IP') || 'Unknown'
    }));

    return new Response(JSON.stringify({
      success: true,
      roomName,
      roomId: roomId.toString(),
      websocketUrl: `/room/${roomName}`
    }), {
      headers: { 'Content-Type': 'application/json' }
    });

  } catch (error) {
    return new Response(JSON.stringify({ error: error.message }), {
      status: 500,
      headers: { 'Content-Type': 'application/json' }
    });
  }
}

async function handleListRooms(env) {
  try {
    const rooms = [];
    const list = await env.ROOM_METADATA.list();

    for (const key of list.keys) {
      const roomData = JSON.parse(await env.ROOM_METADATA.get(key.name));
      rooms.push(roomData);
    }

    return new Response(JSON.stringify({ rooms }), {
      headers: { 'Content-Type': 'application/json' }
    });

  } catch (error) {
    return new Response(JSON.stringify({ error: error.message }), {
      status: 500,
      headers: { 'Content-Type': 'application/json' }
    });
  }
}

function getWebSocketTestPage() {
  return `
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Cloudflare Workers WebSocket Test</title>
    <style>
        body { font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; }
        .container { background: #f5f5f5; padding: 20px; border-radius: 8px; margin-bottom: 20px; }
        .form-group { margin-bottom: 15px; }
        label { display: block; margin-bottom: 5px; font-weight: bold; }
        input, select { width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; }
        button { background: #007cba; color: white; padding: 10px 20px; border: none; border-radius: 4px; cursor: pointer; margin-right: 10px; }
        button:hover { background: #005a87; }
        button:disabled { background: #ccc; cursor: not-allowed; }
        .messages { height: 400px; overflow-y: auto; border: 1px solid #ddd; padding: 10px; background: white; }
        .message { margin-bottom: 10px; padding: 8px; border-radius: 4px; }
        .message.own { background: #e3f2fd; text-align: right; }
        .message.other { background: #f3e5f5; }
        .message.system { background: #fff3e0; font-style: italic; }
        .message.private { background: #e8f5e8; border-left: 4px solid #4caf50; }
        .typing-indicator { color: #666; font-style: italic; }
        .users-list { background: white; padding: 10px; border: 1px solid #ddd; border-radius: 4px; }
        .status { padding: 10px; margin-bottom: 20px; border-radius: 4px; }
        .status.connected { background: #d4edda; color: #155724; }
        .status.disconnected { background: #f8d7da; color: #721c24; }
        .status.connecting { background: #fff3cd; color: #856404; }
    </style>
</head>
<body>
    <h1>🌐 Cloudflare Workers WebSocket Test</h1>

    <div id="connectionStatus" class="status disconnected">
        Disconnected
    </div>

    <div class="container">
        <h2>Connect to Room</h2>
        <div class="form-group">
            <label for="roomName">Room Name:</label>
            <input type="text" id="roomName" placeholder="my-room" required>
        </div>
        <button id="connectBtn" onclick="connect()">Connect</button>
        <button id="disconnectBtn" onclick="disconnect()" disabled>Disconnect</button>
        <button onclick="createRoom()">Create New Room</button>
    </div>

    <div class="container">
        <h2>Chat</h2>
        <div class="form-group">
            <label for="nickname">Nickname:</label>
            <input type="text" id="nickname" placeholder="Enter nickname">
            <button onclick="setNickname()">Set Nickname</button>
        </div>

        <div class="form-group">
            <label for="message">Message:</label>
            <input type="text" id="message" placeholder="Type a message..." onkeypress="if(event.key==='Enter') sendMessage()">
            <button onclick="sendMessage()">Send</button>
        </div>

        <div class="messages" id="messages"></div>
        <div id="typingIndicator" class="typing-indicator"></div>
    </div>

    <div class="container">
        <h2>Users</h2>
        <div class="users-list" id="usersList">No users connected</div>
    </div>

    <script>
        let ws = null;
        let currentRoom = null;
        let mySessionId = null;

        function updateStatus(status, message) {
            const statusEl = document.getElementById('connectionStatus');
            statusEl.className = `status ${status}`;
            statusEl.textContent = message;
        }

        function addMessage(content, type = 'system') {
            const messagesEl = document.getElementById('messages');
            const messageEl = document.createElement('div');
            messageEl.className = `message ${type}`;
            messageEl.innerHTML = content;
            messagesEl.appendChild(messageEl);
            messagesEl.scrollTop = messagesEl.scrollHeight;
        }

        function connect() {
            const roomName = document.getElementById('roomName').value.trim();
            if (!roomName) {
                alert('Please enter a room name');
                return;
            }

            const wsUrl = new URL(window.location.href);
            wsUrl.protocol = wsUrl.protocol.replace('http', 'ws');
            wsUrl.pathname = `/room/${roomName}/socket`;

            ws = new WebSocket(wsUrl.toString());

            updateStatus('connecting', 'Connecting...');
            currentRoom = roomName;

            ws.onopen = () => {
                updateStatus('connected', `Connected to room: ${roomName}`);
                document.getElementById('connectBtn').disabled = true;
                document.getElementById('disconnectBtn').disabled = false;
                addMessage(`Connected to room ${roomName}`, 'system');
            };

            ws.onmessage = (event) => {
                const data = JSON.parse(event.data);
                handleWebSocketMessage(data);
            };

            ws.onclose = () => {
                updateStatus('disconnected', 'Disconnected');
                document.getElementById('connectBtn').disabled = false;
                document.getElementById('disconnectBtn').disabled = true;
                ws = null;
                currentRoom = null;
                mySessionId = null;
                addMessage('Disconnected from server', 'system');
            };

            ws.onerror = (error) => {
                console.error('WebSocket error:', error);
                updateStatus('disconnected', 'Connection error');
                addMessage('Connection error occurred', 'system');
            };
        }

        function disconnect() {
            if (ws) {
                ws.close();
            }
        }

        function handleWebSocketMessage(data) {
            switch (data.type) {
                case 'welcome':
                    mySessionId = data.data.sessionId;
                    addMessage(`Welcome! Your session ID: ${data.data.sessionId}`, 'system');
                    break;

                case 'chatMessage':
                    const isOwn = data.data.sessionId === mySessionId;
                    const messageType = isOwn ? 'own' : 'other';
                    addMessage(`
                        <strong>${data.data.nickname}:</strong> ${data.data.message}
                        <div style="font-size: 0.8em; color: #666;">
                            ${new Date(data.data.timestamp).toLocaleTimeString()}
                        </div>
                    `, messageType);
                    break;

                case 'userJoined':
                    addMessage(`${data.data.user.nickname} joined the room`, 'system');
                    break;

                case 'userLeft':
                    addMessage(`${data.data.user.nickname} left the room`, 'system');
                    break;

                case 'userTyping':
                    if (data.data.isTyping) {
                        document.getElementById('typingIndicator').textContent =
                            `${data.data.nickname} is typing...`;
                    } else {
                        document.getElementById('typingIndicator').textContent = '';
                    }
                    break;

                case 'nicknameChanged':
                    addMessage(`User changed nickname`, 'system');
                    break;

                case 'privateMessage':
                    const isPrivateOwn = data.data.fromSessionId === mySessionId;
                    addMessage(`
                        <div style="color: #2e7d32;">
                            <strong>🔒 Private from ${data.data.fromNickname}:</strong> ${data.data.message}
                        </div>
                    `, 'private');
                    break;

                case 'privateMessageSent':
                    addMessage(`
                        <div style="color: #2e7d32; text-align: right;">
                            <strong>🔒 Private to ${data.data.toSessionId}:</strong> ${data.data.message}
                        </div>
                    `, 'private');
                    break;

                case 'error':
                    addMessage(`Error: ${data.data.message}`, 'system');
                    break;

                default:
                    console.log('Unknown message type:', data);
            }
        }

        function sendMessage() {
            if (!ws || ws.readyState !== WebSocket.OPEN) {
                alert('Not connected');
                return;
            }

            const messageInput = document.getElementById('message');
            const message = messageInput.value.trim();

            if (message) {
                ws.send(JSON.stringify({
                    type: 'chatMessage',
                    data: { message }
                }));
                messageInput.value = '';
            }
        }

        function setNickname() {
            if (!ws || ws.readyState !== WebSocket.OPEN) {
                alert('Not connected');
                return;
            }

            const nicknameInput = document.getElementById('nickname');
            const nickname = nicknameInput.value.trim();

            if (nickname) {
                ws.send(JSON.stringify({
                    type: 'setNickname',
                    data: { nickname }
                }));
            }
        }

        function createRoom() {
            const roomName = prompt('Enter room name:');
            if (!roomName) return;

            fetch('/create-room', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ roomName, maxClients: 50 })
            })
            .then(response => response.json())
            .then(data => {
                if (data.success) {
                    alert(`Room created: ${data.roomName}`);
                    document.getElementById('roomName').value = data.roomName;
                    connect();
                } else {
                    alert('Error: ' + data.error);
                }
            })
            .catch(error => {
                alert('Error: ' + error.message);
            });
        }

        // Handle typing indicator
        let typingTimer;
        document.getElementById('message').addEventListener('input', () => {
            if (ws && ws.readyState === WebSocket.OPEN) {
                ws.send(JSON.stringify({
                    type: 'typing',
                    data: { isTyping: true }
                }));

                clearTimeout(typingTimer);
                typingTimer = setTimeout(() => {
                    ws.send(JSON.stringify({
                        type: 'typing',
                        data: { isTyping: false }
                    }));
                }, 1000);
            }
        });
    </script>
</body>
</html>
`;
}

// wrangler.toml
/*
name = "websocket-server"
main = "src/index.js"
compatibility_date = "2023-10-30"

# Durable Objects
[[durable_objects.bindings]]
name = "CHAT_ROOMS"
class_name = "ChatRoom"

# KV for room metadata
[[kv_namespaces]]
binding = "ROOM_METADATA"
id = "your-kv-namespace-id"
preview_id = "your-preview-kv-namespace-id"
*/

💻 Edge-First CMS Worker typescript

🔴 complex ⭐⭐⭐⭐⭐

Headless CMS with edge caching, drafts, and real-time content delivery

⏱️ 50 min 🏷️ cloudflare, workers, cms, content-management, durable-objects
Prerequisites: Advanced Cloudflare Workers knowledge, TypeScript, CMS concepts, Durable Objects
// Cloudflare Workers Edge-First CMS
// src/index.ts - Content management with edge caching and real-time updates

import { nanoid } from 'nanoid';

interface Content {
  id: string;
  type: 'page' | 'post' | 'component' | 'asset';
  slug: string;
  title: string;
  content: any;
  status: 'draft' | 'published' | 'archived';
  authorId: string;
  authorName: string;
  createdAt: number;
  updatedAt: number;
  publishedAt?: number;
  tags: string[];
  metadata: Record<string, any>;
  seo?: {
    title?: string;
    description?: string;
    keywords?: string[];
  };
}

interface User {
  id: string;
  email: string;
  name: string;
  role: 'admin' | 'editor' | 'author';
  permissions: string[];
  createdAt: number;
  lastLogin?: number;
}

// Durable Object for content storage
export class ContentStore {
  constructor(state: DurableObjectState, env: Env) {
    this.state = state;
    this.env = env;
  }

  private async initializeContent() {
    const hasContent = await this.state.storage.get('initialized');
    if (!hasContent) {
      await this.state.storage.put('initialized', true);
      await this.state.storage.put('content:pages', []);
      await this.state.storage.put('content:posts', []);
      await this.state.storage.put('content:components', []);
      await this.state.storage.put('content:assets', []);
      await this.state.storage.put('users', []);
      console.log('Content store initialized');
    }
  }

  async fetch(request: Request): Promise<Response> {
    await this.initializeContent();

    const url = new URL(request.url);
    const pathname = url.pathname;

    // Handle content API
    if (pathname.startsWith('/api/content/')) {
      return this.handleContentAPI(request, pathname.replace('/api/content/', ''));
    }

    // Handle user authentication
    if (pathname.startsWith('/api/auth/')) {
      return this.handleAuthAPI(request, pathname.replace('/api/auth/', ''));
    }

    // Handle preview requests
    if (pathname.startsWith('/preview/')) {
      return this.handlePreview(request, pathname.replace('/preview/', ''));
    }

    // Handle public content delivery
    if (pathname.startsWith('/content/') || pathname === '/') {
      return this.handlePublicContent(request);
    }

    return new Response('Not Found', { status: 404 });
  }

  private async handleContentAPI(request: Request, endpoint: string): Promise<Response> {
    const method = request.method;
    const url = new URL(request.url);

    // Verify authentication for write operations
    if (['POST', 'PUT', 'DELETE'].includes(method)) {
      const authResult = await this.authenticateRequest(request);
      if (!authResult.success) {
        return new Response(JSON.stringify({ error: authResult.error }), {
          status: 401,
          headers: { 'Content-Type': 'application/json' }
        });
      }
    }

    try {
      switch (true) {
        case endpoint === '' && method === 'GET':
          return this.getContentList(url);

        case endpoint.endsWith('/new') && method === 'POST':
          return this.createContent(request, endpoint.replace('/new', ''));

        case endpoint.match(/^[\w-]+$/) && method === 'GET':
          return this.getContent(endpoint);

        case endpoint.match(/^[\w-]+$/) && method === 'PUT':
          return this.updateContent(endpoint, request);

        case endpoint.match(/^[\w-]+$/) && method === 'DELETE':
          return this.deleteContent(endpoint);

        case endpoint === 'search' && method === 'GET':
          return this.searchContent(url);

        case endpoint === 'tags' && method === 'GET':
          return this.getTags();

        default:
          return new Response('Endpoint not found', { status: 404 });
      }
    } catch (error) {
      console.error('Content API error:', error);
      return new Response(JSON.stringify({
        error: 'Internal server error',
        message: error.message
      }), {
        status: 500,
        headers: { 'Content-Type': 'application/json' }
      });
    }
  }

  private async getContentList(url: URL): Promise<Response> {
    const type = url.searchParams.get('type') || 'all';
    const status = url.searchParams.get('status') || 'published';
    const limit = parseInt(url.searchParams.get('limit') || '20');
    const offset = parseInt(url.searchParams.get('offset') || '0');
    const sortBy = url.searchParams.get('sortBy') || 'updatedAt';
    const sortOrder = url.searchParams.get('sortOrder') || 'desc';
    const tags = url.searchParams.getAll('tags');

    let content: Content[] = [];

    // Get content based on type
    if (type === 'all') {
      const pages = await this.state.storage.get<Content[]>('content:pages') || [];
      const posts = await this.state.storage.get<Content[]>('content:posts') || [];
      const components = await this.state.storage.get<Content[]>('content:components') || [];
      content = [...pages, ...posts, ...components];
    } else {
      content = await this.state.storage.get<Content[]>(`content:${type}s`) || [];
    }

    // Filter by status
    content = content.filter(item => status === 'all' || item.status === status);

    // Filter by tags
    if (tags.length > 0) {
      content = content.filter(item =>
        tags.some(tag => item.tags.includes(tag))
      );
    }

    // Sort
    content.sort((a, b) => {
      const aValue = a[sortBy as keyof Content];
      const bValue = b[sortBy as keyof Content];

      if (sortOrder === 'desc') {
        return bValue - aValue;
      } else {
        return aValue - bValue;
      }
    });

    // Paginate
    const paginatedContent = content.slice(offset, offset + limit);

    return new Response(JSON.stringify({
      content: paginatedContent,
      pagination: {
        total: content.length,
        limit,
        offset,
        hasMore: offset + limit < content.length
      }
    }), {
      headers: {
        'Content-Type': 'application/json',
        'Cache-Control': 'public, max-age=300' // 5 minutes
      }
    });
  }

  private async getContent(slug: string): Promise<Response> {
    const allTypes = ['pages', 'posts', 'components', 'assets'];
    let content: Content | null = null;

    for (const type of allTypes) {
      const items = await this.state.storage.get<Content[]>(`content:${type}`) || [];
      content = items.find(item => item.slug === slug) || null;

      if (content) break;
    }

    if (!content) {
      return new Response(JSON.stringify({ error: 'Content not found' }), {
        status: 404,
        headers: { 'Content-Type': 'application/json' }
      });
    }

    // Only return published content for public requests
    if (content.status !== 'published') {
      const authResult = await this.authenticateRequest(new Request(''));
      if (!authResult.success) {
        return new Response(JSON.stringify({ error: 'Content not available' }), {
          status: 403,
          headers: { 'Content-Type': 'application/json' }
        });
      }
    }

    return new Response(JSON.stringify(content), {
      headers: {
        'Content-Type': 'application/json',
        'Cache-Control': content.status === 'published'
          ? 'public, max-age=3600'  // 1 hour for published
          : 'no-cache'             // No caching for drafts
      }
    });
  }

  private async createContent(request: Request, type: string): Promise<Response> {
    const data = await request.json() as Partial<Content>;
    const user = await this.getCurrentUser(request);

    if (!user) {
      return new Response(JSON.stringify({ error: 'User not found' }), {
        status: 401,
        headers: { 'Content-Type': 'application/json' }
      });
    }

    // Validate required fields
    if (!data.title || !data.slug) {
      return new Response(JSON.stringify({
        error: 'Title and slug are required'
      }), {
        status: 400,
        headers: { 'Content-Type': 'application/json' }
      });
    }

    // Check if slug already exists
    const existingContent = await this.findBySlug(data.slug);
    if (existingContent) {
      return new Response(JSON.stringify({
        error: 'Slug already exists'
      }), {
        status: 409,
        headers: { 'Content-Type': 'application/json' }
      });
    }

    const newContent: Content = {
      id: nanoid(),
      type: type as Content['type'],
      slug: data.slug,
      title: data.title,
      content: data.content || {},
      status: data.status || 'draft',
      authorId: user.id,
      authorName: user.name,
      createdAt: Date.now(),
      updatedAt: Date.now(),
      publishedAt: data.status === 'published' ? Date.now() : undefined,
      tags: data.tags || [],
      metadata: data.metadata || {},
      seo: data.seo
    };

    // Save to appropriate content array
    const storageKey = `content:${type}s`;
    const contentArray = await this.state.storage.get<Content[]>(storageKey) || [];
    contentArray.push(newContent);
    await this.state.storage.put(storageKey, contentArray);

    // Invalidate cache for this content type
    await this.invalidateCache(type);

    return new Response(JSON.stringify(newContent), {
      status: 201,
      headers: { 'Content-Type': 'application/json' }
    });
  }

  private async updateContent(slug: string, request: Request): Promise<Response> {
    const user = await this.getCurrentUser(request);
    if (!user) {
      return new Response(JSON.stringify({ error: 'User not found' }), {
        status: 401,
        headers: { 'Content-Type': 'application/json' }
      });
    }

    const data = await request.json() as Partial<Content>;
    const existingContent = await this.findBySlug(slug);

    if (!existingContent) {
      return new Response(JSON.stringify({ error: 'Content not found' }), {
        status: 404,
        headers: { 'Content-Type': 'application/json' }
      });
    }

    // Check permissions
    if (!this.canEditContent(user, existingContent)) {
      return new Response(JSON.stringify({
        error: 'Insufficient permissions'
      }), {
        status: 403,
        headers: { 'Content-Type': 'application/json' }
      });
    }

    // Update content
    const updatedContent: Content = {
      ...existingContent,
      ...data,
      id: existingContent.id, // Don't allow ID changes
      slug: data.slug || existingContent.slug,
      updatedAt: Date.now(),
      authorId: existingContent.authorId, // Don't allow author changes
      authorName: existingContent.authorName
    };

    // Handle status changes
    if (data.status && data.status !== existingContent.status) {
      if (data.status === 'published' && existingContent.status !== 'published') {
        updatedContent.publishedAt = Date.now();

        // Notify subscribers about new content
        await this.notifySubscribers('content_published', updatedContent);
      }
    }

    // Update in storage
    const storageKey = `content:${existingContent.type}s`;
    const contentArray = await this.state.storage.get<Content[]>(storageKey) || [];
    const index = contentArray.findIndex(item => item.slug === slug);

    if (index !== -1) {
      contentArray[index] = updatedContent;
      await this.state.storage.put(storageKey, contentArray);
    }

    // Invalidate cache
    await this.invalidateCache(existingContent.type);

    return new Response(JSON.stringify(updatedContent), {
      headers: { 'Content-Type': 'application/json' }
    });
  }

  private async deleteContent(slug: string): Promise<Response> {
    const user = await this.getCurrentUser(new Request(''));
    if (!user || user.role !== 'admin') {
      return new Response(JSON.stringify({
        error: 'Only admins can delete content'
      }), {
        status: 403,
        headers: { 'Content-Type': 'application/json' }
      });
    }

    const existingContent = await this.findBySlug(slug);
    if (!existingContent) {
      return new Response(JSON.stringify({ error: 'Content not found' }), {
        status: 404,
        headers: { 'Content-Type': 'application/json' }
      });
    }

    // Remove from storage
    const storageKey = `content:${existingContent.type}s`;
    const contentArray = await this.state.storage.get<Content[]>(storageKey) || [];
    const filteredArray = contentArray.filter(item => item.slug !== slug);

    await this.state.storage.put(storageKey, filteredArray);

    // Invalidate cache
    await this.invalidateCache(existingContent.type);

    return new Response(JSON.stringify({
      success: true,
      message: 'Content deleted successfully'
    }), {
      headers: { 'Content-Type': 'application/json' }
    });
  }

  private async searchContent(url: URL): Promise<Response> {
    const query = url.searchParams.get('q')?.toLowerCase() || '';
    const type = url.searchParams.get('type') || 'all';
    const limit = parseInt(url.searchParams.get('limit') || '10');

    if (!query) {
      return new Response(JSON.stringify({ error: 'Search query is required' }), {
        status: 400,
        headers: { 'Content-Type': 'application/json' }
      });
    }

    let allContent: Content[] = [];

    // Get all content based on type
    if (type === 'all') {
      const pages = await this.state.storage.get<Content[]>('content:pages') || [];
      const posts = await this.state.storage.get<Content[]>('content:posts') || [];
      const components = await this.state.storage.get<Content[]>('content:components') || [];
      allContent = [...pages, ...posts, ...components];
    } else {
      allContent = await this.state.storage.get<Content[]>(`content:${type}s`) || [];
    }

    // Search in title and content
    const searchResults = allContent.filter(item => {
      if (item.status !== 'published') return false;

      const titleMatch = item.title.toLowerCase().includes(query);
      const contentMatch = JSON.stringify(item.content).toLowerCase().includes(query);
      const tagsMatch = item.tags.some(tag => tag.toLowerCase().includes(query));

      return titleMatch || contentMatch || tagsMatch;
    }).slice(0, limit);

    return new Response(JSON.stringify({
      query,
      results: searchResults,
      total: searchResults.length
    }), {
      headers: {
        'Content-Type': 'application/json',
        'Cache-Control': 'public, max-age=600' // 10 minutes
      }
    });
  }

  private async getTags(): Promise<Response> {
    const allTypes = ['pages', 'posts', 'components'];
    const allTags = new Set<string>();

    for (const type of allTypes) {
      const items = await this.state.storage.get<Content[]>(`content:${type}s`) || [];
      items.forEach(item => {
        item.tags.forEach(tag => allTags.add(tag));
      });
    }

    const sortedTags = Array.from(allTags).sort();

    return new Response(JSON.stringify(sortedTags), {
      headers: {
        'Content-Type': 'application/json',
        'Cache-Control': 'public, max-age=3600' // 1 hour
      }
    });
  }

  private async handlePublicContent(request: Request): Promise<Response> {
    const url = new URL(request.url);
    const pathname = url.pathname;

    // Serve specific content by slug
    if (pathname !== '/' && pathname.startsWith('/content/')) {
      const slug = pathname.replace('/content/', '');
      const content = await this.findBySlug(slug);

      if (!content || content.status !== 'published') {
        return new Response('Content not found', { status: 404 });
      }

      // Render content based on type
      switch (content.type) {
        case 'page':
          return this.renderPage(content, url);
        case 'post':
          return this.renderPost(content, url);
        case 'component':
          return this.renderComponent(content, url);
        default:
          return new Response(JSON.stringify(content), {
            headers: { 'Content-Type': 'application/json' }
          });
      }
    }

    // Serve homepage
    if (pathname === '/') {
      return this.renderHomepage(url);
    }

    return new Response('Not found', { status: 404 });
  }

  private async renderPage(page: Content, url: URL): Promise<Response> {
    const html = `
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>${page.seo?.title || page.title}</title>
    <meta name="description" content="${page.seo?.description || page.content.excerpt || ''}">
    <meta name="keywords" content="${(page.seo?.keywords || []).join(', ')}">
    <link rel="stylesheet" href="/static/styles.css">
</head>
<body>
    <header>
        <nav>
            <a href="/">Home</a>
            <a href="/content/blog">Blog</a>
            <a href="/admin" style="display: none;">Admin</a>
        </nav>
    </header>

    <main>
        <article class="page">
            <h1>${page.title}</h1>
            <div class="content">
                ${this.renderContent(page.content)}
            </div>
            ${page.tags.length > 0 ? `
            <div class="tags">
                ${page.tags.map(tag => `<span class="tag">${tag}</span>`).join('')}
            </div>
            ` : ''}
        </article>
    </main>

    <footer>
        <p>&copy; ${new Date().getFullYear()} Edge CMS. Powered by Cloudflare Workers.</p>
    </footer>

    <script src="/static/cms.js"></script>
</body>
</html>
`;

    return new Response(html, {
      headers: {
        'Content-Type': 'text/html',
        'Cache-Control': 'public, max-age=3600'
      }
    });
  }

  private renderContent(content: any): string {
    if (typeof content === 'string') {
      return content;
    }

    // Handle structured content
    if (content.blocks && Array.isArray(content.blocks)) {
      return content.blocks.map((block: any) => {
        switch (block.type) {
          case 'heading':
            return `<h${block.level || 2}>${block.text}</h${block.level || 2}>`;
          case 'paragraph':
            return `<p>${block.text}</p>`;
          case 'image':
            return `<img src="${block.src}" alt="${block.alt || ''}" />`;
          case 'code':
            return `<pre><code>${block.code}</code></pre>`;
          case 'list':
            return `<ul>${block.items.map((item: string) => `<li>${item}</li>`).join('')}</ul>`;
          default:
            return `<div>${JSON.stringify(block)}</div>`;
        }
      }).join('\n');
    }

    return JSON.stringify(content);
  }

  // Helper methods
  private async findBySlug(slug: string): Promise<Content | null> {
    const allTypes = ['pages', 'posts', 'components', 'assets'];

    for (const type of allTypes) {
      const items = await this.state.storage.get<Content[]>(`content:${type}s`) || [];
      const found = items.find(item => item.slug === slug);
      if (found) return found;
    }

    return null;
  }

  private async authenticateRequest(request: Request): Promise<{ success: boolean; error?: string; user?: User }> {
    try {
      const authHeader = request.headers.get('Authorization');
      if (!authHeader || !authHeader.startsWith('Bearer ')) {
        return { success: false, error: 'No authorization header' };
      }

      const token = authHeader.replace('Bearer ', '');

      // Simple token validation (in production, use proper JWT validation)
      const users = await this.state.storage.get<User[]>('users') || [];
      const user = users.find(u => u.email === token); // Simplified - using email as token

      if (!user) {
        return { success: false, error: 'Invalid token' };
      }

      return { success: true, user };
    } catch (error) {
      return { success: false, error: 'Authentication error' };
    }
  }

  private async getCurrentUser(request: Request): Promise<User | null> {
    const auth = await this.authenticateRequest(request);
    return auth.user || null;
  }

  private canEditContent(user: User, content: Content): boolean {
    if (user.role === 'admin') return true;
    if (user.role === 'editor') return true;
    if (user.role === 'author' && content.authorId === user.id) return true;
    return false;
  }

  private async invalidateCache(type: string): Promise<void> {
    // In a real implementation, you would invalidate Cloudflare cache
    console.log(`Cache invalidated for type: ${type}`);
  }

  private async notifySubscribers(event: string, content: Content): Promise<void> {
    // In a real implementation, send webhook notifications
    console.log(`Notification sent for ${event}: ${content.title}`);
  }

  private async handleAuthAPI(request: Request, endpoint: string): Promise<Response> {
    // Authentication API implementation
    // This would include login, logout, user management, etc.
    return new Response(JSON.stringify({ error: 'Auth API not implemented' }), {
      status: 501,
      headers: { 'Content-Type': 'application/json' }
    });
  }

  private async handlePreview(request: Request, slug: string): Promise<Response> {
    const content = await this.findBySlug(slug);

    if (!content) {
      return new Response('Content not found', { status: 404 });
    }

    // Show preview even for drafts (in production, check permissions)
    return this.renderPage(content, new URL(request.url));
  }

  private async renderHomepage(url: URL): Promise<Response> {
    // Get latest posts
    const posts = await this.state.storage.get<Content[]>('content:posts') || [];
    const publishedPosts = posts
      .filter(post => post.status === 'published')
      .sort((a, b) => (b.publishedAt || 0) - (a.publishedAt || 0))
      .slice(0, 5);

    const html = `
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Edge CMS - Home</title>
    <meta name="description" content="Edge-first content management system powered by Cloudflare Workers">
    <link rel="stylesheet" href="/static/styles.css">
</head>
<body>
    <header>
        <nav>
            <a href="/">Home</a>
            <a href="/content/blog">Blog</a>
            <a href="/admin">Admin</a>
        </nav>
    </header>

    <main>
        <section class="hero">
            <h1>Welcome to Edge CMS</h1>
            <p>Lightning-fast content delivery powered by Cloudflare Workers</p>
        </section>

        <section class="latest-posts">
            <h2>Latest Posts</h2>
            ${publishedPosts.map(post => `
            <article class="post-preview">
                <h3><a href="/content/${post.slug}">${post.title}</a></h3>
                <p>${post.content.excerpt || ''}</p>
                <div class="meta">
                    By ${post.authorName} • ${new Date(post.publishedAt || post.createdAt).toLocaleDateString()}
                </div>
            </article>
            `).join('')}
        </section>
    </main>

    <footer>
        <p>&copy; ${new Date().getFullYear()} Edge CMS. Powered by Cloudflare Workers.</p>
    </footer>
</body>
</html>
`;

    return new Response(html, {
      headers: {
        'Content-Type': 'text/html',
        'Cache-Control': 'public, max-age=300'
      }
    });
  }

  private async renderPost(post: Content, url: URL): Promise<Response> {
    // Similar to renderPage but optimized for blog posts
    return this.renderPage(post, url);
  }

  private async renderComponent(component: Content, url: URL): Promise<Response> {
    // Render reusable components
    return new Response(this.renderContent(component.content), {
      headers: {
        'Content-Type': 'text/html',
        'Cache-Control': 'public, max-age=3600'
      }
    });
  }
}

// Main worker
export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
    const url = new URL(request.url);
    const pathname = url.pathname;

    // Route to content store Durable Object
    if (pathname.startsWith('/api/') ||
        pathname.startsWith('/content/') ||
        pathname.startsWith('/preview/') ||
        pathname === '/') {

      const contentStore = env.CONTENT_STORE.get(env.CONTENT_STORE.idFromName('default'));
      return contentStore.fetch(request);
    }

    // Serve static assets
    if (pathname.startsWith('/static/')) {
      // In production, you would serve from R2 storage
      return new Response('Static file not found', { status: 404 });
    }

    return new Response('Not Found', { status: 404 });
  }
};

// Environment types
interface Env {
  CONTENT_STORE: DurableObjectNamespace;
  AUTH_SECRET: string;
  // Add other environment variables as needed
}

// wrangler.toml
/*
name = "edge-cms"
main = "src/index.ts"
compatibility_date = "2023-10-30"

# Durable Objects
[[durable_objects.bindings]]
name = "CONTENT_STORE"
class_name = "ContentStore"

# Environment variables
[vars]
ENVIRONMENT = "production"

# KV for caching (optional)
[[kv_namespaces]]
binding = "CMS_CACHE"
id = "your-kv-namespace-id"
preview_id = "your-preview-kv-namespace-id"

# R2 for asset storage (optional)
[[r2_buckets]]
binding = "ASSETS"
bucket_name = "your-asset-bucket"
*/