Exemples Cloudflare Pages

Exemples d'applications web full-stack Cloudflare Pages avec Functions, déploiement de bord et intégration de frameworks web modernes

💻 Hello World de Base javascript

🟢 simple

Introduction simple aux applications Cloudflare Pages

// Cloudflare Pages Hello World
// Complete setup with static site and functions

// 1. Public/index.html - Main HTML file
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Cloudflare Pages Hello World</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
            line-height: 1.6;
            color: #333;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            min-height: 100vh;
            display: flex;
            align-items: center;
            justify-content: center;
        }

        .container {
            background: white;
            border-radius: 10px;
            padding: 2rem;
            box-shadow: 0 20px 40px rgba(0,0,0,0.1);
            max-width: 500px;
            width: 90%;
            text-align: center;
        }

        h1 {
            color: #667eea;
            margin-bottom: 1rem;
            font-size: 2rem;
        }

        .logo {
            width: 80px;
            height: 80px;
            margin: 0 auto 1rem;
            background: linear-gradient(135deg, #667eea, #764ba2);
            border-radius: 50%;
            display: flex;
            align-items: center;
            justify-content: center;
            color: white;
            font-size: 2rem;
            font-weight: bold;
        }

        .feature-grid {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
            gap: 1rem;
            margin: 2rem 0;
        }

        .feature {
            padding: 1rem;
            background: #f8f9fa;
            border-radius: 8px;
            border: 2px solid transparent;
            transition: all 0.3s ease;
        }

        .feature:hover {
            border-color: #667eea;
            transform: translateY(-2px);
        }

        .btn {
            display: inline-block;
            padding: 12px 24px;
            background: linear-gradient(135deg, #667eea, #764ba2);
            color: white;
            text-decoration: none;
            border-radius: 6px;
            margin: 0.5rem;
            transition: transform 0.2s ease;
            border: none;
            cursor: pointer;
            font-size: 1rem;
        }

        .btn:hover {
            transform: translateY(-2px);
        }

        .btn-secondary {
            background: #6c757d;
        }

        .info {
            background: #e3f2fd;
            border-left: 4px solid #667eea;
            padding: 1rem;
            margin: 1rem 0;
            text-align: left;
        }

        .loading {
            display: none;
            color: #667eea;
        }

        .response {
            margin-top: 1rem;
            padding: 1rem;
            background: #f8f9fa;
            border-radius: 6px;
            text-align: left;
            white-space: pre-wrap;
            font-family: 'Courier New', monospace;
            font-size: 0.9rem;
        }

        @media (max-width: 480px) {
            .container {
                padding: 1rem;
            }

            h1 {
                font-size: 1.5rem;
            }
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="logo">☁️</div>
        <h1>Cloudflare Pages</h1>
        <p>Deploy static sites and serverless functions at the edge</p>

        <div class="feature-grid">
            <div class="feature">
                <h3>⚡ Lightning Fast</h3>
                <p>Global CDN with automatic HTTPS</p>
            </div>
            <div class="feature">
                <h3>🚀 Zero Config</h3>
                <p>Deploy from Git in seconds</p>
            </div>
            <div class="feature">
                <h3>📦 Functions</h3>
                <p>Serverless edge computing</p>
            </div>
            <div class="feature">
                <h3>🔒 Secure</h3>
                <p>DDoS protection and WAF</p>
            </div>
        </div>

        <div class="info">
            <strong>Try the serverless functions:</strong><br>
            This page includes several API endpoints powered by Cloudflare Pages Functions.
        </div>

        <button class="btn" onclick="testAPI('/api/hello')">
            Test Hello API
        </button>
        <button class="btn" onclick="testAPI('/api/time')">
            Get Server Time
        </button>
        <button class="btn" onclick="testAPI('/api/info')">
            Get Request Info
        </button>
        <button class="btn btn-secondary" onclick="testAPI('/api/echo', {message: 'Hello from Cloudflare Pages!'})">
            Test Echo API
        </button>

        <div class="loading" id="loading">Loading...</div>
        <div class="response" id="response" style="display: none;"></div>
    </div>

    <script>
        async function testAPI(endpoint, data = null) {
            const loading = document.getElementById('loading');
            const response = document.getElementById('response');

            loading.style.display = 'block';
            response.style.display = 'none';

            try {
                const options = {
                    method: data ? 'POST' : 'GET',
                    headers: {
                        'Content-Type': 'application/json',
                    },
                };

                if (data) {
                    options.body = JSON.stringify(data);
                }

                const res = await fetch(endpoint, options);
                const result = await res.json();

                response.textContent = JSON.stringify(result, null, 2);
                response.style.display = 'block';
            } catch (error) {
                response.textContent = `Error: ${error.message}`;
                response.style.display = 'block';
            } finally {
                loading.style.display = 'none';
            }
        }

        // Get initial info
        window.addEventListener('load', () => {
            testAPI('/api/info');
        });
    </script>
</body>
</html>

// 2. Functions/api/hello.js - Simple hello function
export function onRequest(context) {
  return new Response(
    JSON.stringify({
      message: "Hello from Cloudflare Pages Functions! 🎉",
      method: context.request.method,
      url: context.request.url,
      timestamp: new Date().toISOString(),
      environment: context.env.CF_PAGES ? 'production' : 'preview'
    }),
    {
      headers: {
        "Content-Type": "application/json",
        "Cache-Control": "public, max-age=60"
      }
    }
  );
}

// 3. Functions/api/time.js - Server time function
export function onRequest() {
  const now = new Date();

  return new Response(
    JSON.stringify({
      serverTime: now.toISOString(),
      unixTimestamp: Math.floor(now.getTime() / 1000),
      timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
      formatted: {
        date: now.toLocaleDateString(),
        time: now.toLocaleTimeString(),
        datetime: now.toLocaleString()
      }
    }),
    {
      headers: {
        "Content-Type": "application/json"
      }
    }
  );
}

// 4. Functions/api/info.js - Request information
export function onRequest(context) {
  const request = context.request;
  const url = new URL(request.url);

  return new Response(
    JSON.stringify({
      request: {
        method: request.method,
        url: request.url,
        headers: Object.fromEntries(request.headers.entries()),
        cf: request.cf
      },
      environment: {
        pages: context.env.CF_PAGES ? true : false,
        branch: context.env.CF_PAGES_BRANCH || 'unknown',
        commitSha: context.env.CF_PAGES_COMMIT_SHA || 'unknown'
      },
      location: {
        country: request.cf.country,
        city: request.cf.city,
        colo: request.cf.colo,
        timezone: request.cf.timezone
      },
      device: {
        mobile: request.cf.device.mobile,
        model: request.cf.device.model,
        brand: request.cf.device.brand
      }
    }),
    {
      headers: {
        "Content-Type": "application/json"
      }
    }
  );
}

// 5. Functions/api/echo.js - Echo function
export async function onRequest(context) {
  const request = context.request;

  if (request.method !== 'POST') {
    return new Response(
      JSON.stringify({ error: 'Method not allowed' }),
      { status: 405, headers: { "Content-Type": "application/json" } }
    );
  }

  try {
    const body = await request.json();

    return new Response(
      JSON.stringify({
        echo: body,
        receivedAt: new Date().toISOString(),
        requestInfo: {
          method: request.method,
          contentType: request.headers.get('content-type'),
          userAgent: request.headers.get('user-agent')
        }
      }),
      {
        headers: {
          "Content-Type": "application/json"
        }
      }
    );
  } catch (error) {
    return new Response(
      JSON.stringify({
        error: 'Invalid JSON',
        message: error.message
      }),
      {
        status: 400,
        headers: { "Content-Type": "application/json" }
      }
    );
  }
}

// 6. Functions/api/proxy.js - API proxy example
export async function onRequest(context) {
  const url = new URL(context.request.url);
  const targetUrl = url.searchParams.get('url');

  if (!targetUrl) {
    return new Response(
      JSON.stringify({
        error: 'Missing url parameter',
        usage: 'GET /api/proxy?url=https://api.example.com/data'
      }),
      {
        status: 400,
        headers: { "Content-Type": "application/json" }
      }
    );
  }

  try {
    const response = await fetch(targetUrl);
    const data = await response.text();

    return new Response(data, {
      status: response.status,
      headers: {
        "Content-Type": response.headers.get('content-type') || 'text/plain',
        "X-Proxy-For": targetUrl
      }
    });
  } catch (error) {
    return new Response(
      JSON.stringify({
        error: 'Proxy request failed',
        message: error.message,
        targetUrl
      }),
      {
        status: 500,
        headers: { "Content-Type": "application/json" }
      }
    );
  }
}

// 7. Functions/api/image.js - Image processing
export async function onRequest(context) {
  const request = context.request;
  const url = new URL(request.url);
  const imageUrl = url.searchParams.get('url');
  const width = parseInt(url.searchParams.get('width')) || 800;
  const quality = parseInt(url.searchParams.get('quality')) || 80;

  if (!imageUrl) {
    return new Response(
      JSON.stringify({
        error: 'Missing url parameter',
        usage: 'GET /api/image?url=https://example.com/image.jpg&width=800&quality=80'
      }),
      {
        status: 400,
        headers: { "Content-Type": "application/json" }
      }
    );
  }

  try {
    // Fetch original image
    const imageResponse = await fetch(imageUrl);
    if (!imageResponse.ok) {
      throw new Error(`Failed to fetch image: ${imageResponse.status}`);
    }

    const originalImage = await imageResponse.arrayBuffer();

    // For now, just pass through the image
    // In a real application, you could use Cloudflare's Image Resizing
    return new Response(originalImage, {
      headers: {
        "Content-Type": imageResponse.headers.get('content-type') || 'image/jpeg',
        "Cache-Control": "public, max-age=31536000" // 1 year
      }
    });
  } catch (error) {
    return new Response(
      JSON.stringify({
        error: 'Image processing failed',
        message: error.message,
        imageUrl
      }),
      {
        status: 500,
        headers: { "Content-Type": "application/json" }
      }
    );
  }
}

💻 Pages Functions javascript

🟡 intermediate

Fonctions côté serveur et routes API

// Cloudflare Pages Advanced Functions
// Complete API with middleware, auth, and database integration

// 1. Functions/_middleware.js - Global middleware
import { getCookie, setCookie } from 'https://deno.land/[email protected]/http/cookie.ts';

export async function onRequest(context) {
  const { request, env, next, params } = context;
  const url = new URL(request.url);
  const pathname = url.pathname;

  // CORS middleware
  const corsHeaders = {
    'Access-Control-Allow-Origin': '*',
    'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
    'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Custom-Header',
  };

  // Handle preflight requests
  if (request.method === 'OPTIONS') {
    return new Response(null, { headers: corsHeaders });
  }

  // Authentication middleware for protected routes
  if (pathname.startsWith('/api/protected')) {
    const authHeader = request.headers.get('Authorization');

    if (!authHeader || !authHeader.startsWith('Bearer ')) {
      return new Response(
        JSON.stringify({ error: 'Missing or invalid authorization header' }),
        {
          status: 401,
          headers: { 'Content-Type': 'application/json', ...corsHeaders }
        }
      );
    }

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

    try {
      const user = await verifyJWT(token, env.JWT_SECRET);
      context.user = user;
    } catch (error) {
      return new Response(
        JSON.stringify({ error: 'Invalid or expired token' }),
        {
          status: 401,
          headers: { 'Content-Type': 'application/json', ...corsHeaders }
        }
      );
    }
  }

  // Rate limiting middleware
  if (pathname.startsWith('/api/')) {
    const clientIP = request.headers.get('CF-Connecting-IP') || request.headers.get('X-Forwarded-For');
    const key = `rate_limit:${clientIP}:${pathname}`;

    const limit = await env.RATE_LIMITER.get(key);
    const current = parseInt(limit?.value || '0');

    if (current > 100) { // 100 requests per minute
      return new Response(
        JSON.stringify({ error: 'Rate limit exceeded' }),
        {
          status: 429,
          headers: {
            'Content-Type': 'application/json',
            'Retry-After': '60',
            ...corsHeaders
          }
        }
      );
    }

    await env.RATE_LIMITER.put(key, (current + 1).toString(), { expirationTtl: 60 });
  }

  // Continue to the next handler
  const response = await next();

  // Add CORS headers to response
  Object.entries(corsHeaders).forEach(([key, value]) => {
    response.headers.set(key, value);
  });

  return response;
}

// 2. Functions/api/auth/login.js - Authentication
export async function onRequest(context) {
  const { request, env } = context;

  if (request.method !== 'POST') {
    return new Response(
      JSON.stringify({ error: 'Method not allowed' }),
      { status: 405, headers: { 'Content-Type': 'application/json' } }
    );
  }

  try {
    const { email, password } = await request.json();

    // Validate input
    if (!email || !password) {
      return new Response(
        JSON.stringify({ error: 'Email and password are required' }),
        { status: 400, headers: { 'Content-Type': 'application/json' } }
      );
    }

    // Find user in database
    const user = await env.DB.prepare(
      'SELECT id, email, password_hash, name FROM users WHERE email = ?'
    ).bind(email).first();

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

    // Verify password (using a simple hash for demo - use bcrypt in production)
    const passwordHash = await hashString(password);
    if (passwordHash !== user.password_hash) {
      return new Response(
        JSON.stringify({ error: 'Invalid credentials' }),
        { status: 401, headers: { 'Content-Type': 'application/json' } }
      );
    }

    // Generate JWT
    const token = await generateJWT(
      { userId: user.id, email: user.email },
      env.JWT_SECRET,
      '24h'
    );

    // Update last login
    await env.DB.prepare(
      'UPDATE users SET last_login = ? WHERE id = ?'
    ).bind(new Date().toISOString(), user.id).run();

    return new Response(
      JSON.stringify({
        message: 'Login successful',
        token,
        user: {
          id: user.id,
          email: user.email,
          name: user.name
        }
      }),
      {
        status: 200,
        headers: {
          'Content-Type': 'application/json',
          'Set-Cookie': `auth_token=${token}; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=86400`
        }
      }
    );

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

// 3. Functions/api/posts/index.js - Posts CRUD API
export async function onRequest(context) {
  const { request, env, user } = context;

  if (request.method === 'GET') {
    // Get posts with pagination
    const url = new URL(request.url);
    const page = parseInt(url.searchParams.get('page')) || 1;
    const limit = parseInt(url.searchParams.get('limit')) || 10;
    const offset = (page - 1) * limit;

    try {
      const posts = await env.DB.prepare(`
        SELECT p.*, u.name as author_name, u.email as author_email
        FROM posts p
        JOIN users u ON p.author_id = u.id
        WHERE p.published = true
        ORDER BY p.created_at DESC
        LIMIT ? OFFSET ?
      `).bind(limit, offset).all();

      const totalResult = await env.DB.prepare(
        'SELECT COUNT(*) as total FROM posts WHERE published = true'
      ).first();

      return new Response(
        JSON.stringify({
          posts: posts.results,
          pagination: {
            page,
            limit,
            total: totalResult.total,
            pages: Math.ceil(totalResult.total / limit)
          }
        }),
        { headers: { 'Content-Type': 'application/json' } }
      );

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

  if (request.method === 'POST') {
    // Create new post (protected route)
    if (!user) {
      return new Response(
        JSON.stringify({ error: 'Authentication required' }),
        { status: 401, headers: { 'Content-Type': 'application/json' } }
      );
    }

    try {
      const { title, content, excerpt, tags } = await request.json();

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

      const postId = crypto.randomUUID();
      const now = new Date().toISOString();

      await env.DB.prepare(`
        INSERT INTO posts (id, title, content, excerpt, tags, author_id, created_at, updated_at, published)
        VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
      `).bind(
        postId,
        title,
        content,
        excerpt || null,
        tags ? JSON.stringify(tags) : null,
        user.userId,
        now,
        now,
        false // Draft by default
      ).run();

      const post = await env.DB.prepare(
        'SELECT * FROM posts WHERE id = ?'
      ).bind(postId).first();

      return new Response(
        JSON.stringify({
          message: 'Post created successfully',
          post
        }),
        {
          status: 201,
          headers: { 'Content-Type': 'application/json' }
        }
      );

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

  return new Response(
    JSON.stringify({ error: 'Method not allowed' }),
    { status: 405, headers: { 'Content-Type': 'application/json' } }
  );
}

// 4. Functions/api/posts/[id].js - Individual post operations
export async function onRequest(context) {
  const { request, env, params, user } = context;
  const postId = params.id;

  if (request.method === 'GET') {
    try {
      const post = await env.DB.prepare(`
        SELECT p.*, u.name as author_name, u.email as author_email
        FROM posts p
        JOIN users u ON p.author_id = u.id
        WHERE p.id = ? AND (p.published = true OR p.author_id = ?)
      `).bind(postId, user?.userId).first();

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

      // Increment view count
      await env.DB.prepare(
        'UPDATE posts SET views = views + 1 WHERE id = ?'
      ).bind(postId).run();

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

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

  if (request.method === 'PUT') {
    // Update post (protected route)
    if (!user) {
      return new Response(
        JSON.stringify({ error: 'Authentication required' }),
        { status: 401, headers: { 'Content-Type': 'application/json' } }
      );
    }

    try {
      const existingPost = await env.DB.prepare(
        'SELECT author_id FROM posts WHERE id = ?'
      ).bind(postId).first();

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

      if (existingPost.author_id !== user.userId) {
        return new Response(
          JSON.stringify({ error: 'Unauthorized to edit this post' }),
          { status: 403, headers: { 'Content-Type': 'application/json' } }
        );
      }

      const { title, content, excerpt, tags, published } = await request.json();

      await env.DB.prepare(`
        UPDATE posts
        SET title = ?, content = ?, excerpt = ?, tags = ?, published = ?, updated_at = ?
        WHERE id = ?
      `).bind(
        title,
        content,
        excerpt || null,
        tags ? JSON.stringify(tags) : null,
        published || false,
        new Date().toISOString(),
        postId
      ).run();

      const updatedPost = await env.DB.prepare(
        'SELECT * FROM posts WHERE id = ?'
      ).bind(postId).first();

      return new Response(
        JSON.stringify({
          message: 'Post updated successfully',
          post: updatedPost
        }),
        { headers: { 'Content-Type': 'application/json' } }
      );

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

  if (request.method === 'DELETE') {
    // Delete post (protected route)
    if (!user) {
      return new Response(
        JSON.stringify({ error: 'Authentication required' }),
        { status: 401, headers: { 'Content-Type': 'application/json' } }
      );
    }

    try {
      const existingPost = await env.DB.prepare(
        'SELECT author_id FROM posts WHERE id = ?'
      ).bind(postId).first();

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

      if (existingPost.author_id !== user.userId) {
        return new Response(
          JSON.stringify({ error: 'Unauthorized to delete this post' }),
          { status: 403, headers: { 'Content-Type': 'application/json' } }
        );
      }

      await env.DB.prepare('DELETE FROM posts WHERE id = ?').bind(postId).run();

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

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

  return new Response(
    JSON.stringify({ error: 'Method not allowed' }),
    { status: 405, headers: { 'Content-Type': 'application/json' } }
  );
}

// 5. Functions/api/upload.js - File upload with image processing
export async function onRequest(context) {
  const { request, env, user } = context;

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

  if (request.method !== 'POST') {
    return new Response(
      JSON.stringify({ error: 'Method not allowed' }),
      { status: 405, headers: { 'Content-Type': 'application/json' } }
    );
  }

  try {
    const formData = await request.formData();
    const file = formData.get('file');

    if (!file) {
      return new Response(
        JSON.stringify({ error: 'No file provided' }),
        { status: 400, headers: { 'Content-Type': 'application/json' } }
      );
    }

    // Validate file type
    const allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
    if (!allowedTypes.includes(file.type)) {
      return new Response(
        JSON.stringify({ error: 'Invalid file type. Only images are allowed.' }),
        { status: 400, headers: { 'Content-Type': 'application/json' } }
      );
    }

    // Validate file size (10MB max)
    if (file.size > 10 * 1024 * 1024) {
      return new Response(
        JSON.stringify({ error: 'File too large. Maximum size is 10MB.' }),
        { status: 400, headers: { 'Content-Type': 'application/json' } }
      );
    }

    // Generate unique filename
    const fileExtension = file.name.split('.').pop();
    const fileName = `${user.userId}_${Date.now()}.${fileExtension}`;

    // Upload to R2 (or any storage service)
    const uploadUrl = `https://${env.R2_BUCKET_NAME}.${env.R2_ACCOUNT_ID}.r2.cloudflarestorage.com/${fileName}`;

    await fetch(uploadUrl, {
      method: 'PUT',
      headers: {
        'Content-Type': file.type,
        'X-Amz-Content-Sha256': 'UNSIGNED-PAYLOAD'
      },
      body: file
    });

    // Create database record
    const fileId = crypto.randomUUID();
    await env.DB.prepare(`
      INSERT INTO files (id, user_id, filename, original_name, file_type, file_size, upload_url, created_at)
      VALUES (?, ?, ?, ?, ?, ?, ?, ?)
    `).bind(
      fileId,
      user.userId,
      fileName,
      file.name,
      file.type,
      file.size,
      uploadUrl,
      new Date().toISOString()
    ).run();

    return new Response(
      JSON.stringify({
        message: 'File uploaded successfully',
        file: {
          id: fileId,
          filename: fileName,
          originalName: file.name,
          fileType: file.type,
          fileSize: file.size,
          uploadUrl
        }
      }),
      {
        status: 201,
        headers: { 'Content-Type': 'application/json' }
      }
    );

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

// 6. Utility functions
async function generateJWT(payload, secret, expiresIn = '1h') {
  const header = { alg: 'HS256', typ: 'JWT' };
  const now = Math.floor(Date.now() / 1000);
  const exp = now + (expiresIn === '24h' ? 86400 : 3600);

  const encodedHeader = btoa(JSON.stringify(header));
  const encodedPayload = btoa(JSON.stringify({ ...payload, iat: now, exp }));

  const signature = await hmacSha256(
    `${encodedHeader}.${encodedPayload}`,
    secret
  );

  return `${encodedHeader}.${encodedPayload}.${signature}`;
}

async function verifyJWT(token, secret) {
  const [encodedHeader, encodedPayload, signature] = token.split('.');

  const expectedSignature = await hmacSha256(
    `${encodedHeader}.${encodedPayload}`,
    secret
  );

  if (signature !== expectedSignature) {
    throw new Error('Invalid signature');
  }

  const payload = JSON.parse(atob(encodedPayload));

  if (payload.exp < Math.floor(Date.now() / 1000)) {
    throw new Error('Token expired');
  }

  return payload;
}

async function hmacSha256(message, secret) {
  const encoder = new TextEncoder();
  const keyData = encoder.encode(secret);
  const messageData = encoder.encode(message);

  const key = await crypto.subtle.importKey(
    'raw',
    keyData,
    { name: 'HMAC', hash: 'SHA-256' },
    false,
    ['sign']
  );

  const signature = await crypto.subtle.sign('HMAC', key, messageData);
  return btoa(String.fromCharCode(...new Uint8Array(signature)));
}

async function hashString(str) {
  const encoder = new TextEncoder();
  const data = encoder.encode(str);
  const hash = await crypto.subtle.digest('SHA-256', data);
  return btoa(String.fromCharCode(...new Uint8Array(hash)));
}

💻 Génération de Site Statique

🟡 intermediate

Construire des sites statiques avec des frameworks modernes

// Cloudflare Pages Static Site with Astro
// astro.config.mjs - Astro configuration

import { defineConfig } from 'astro/config';
import react from '@astrojs/react';
import tailwind from '@astrojs/tailwind';
import sitemap from '@astrojs/sitemap';
import { imagetools } from 'astro-image-tools';

export default defineConfig({
  // Build options
  output: 'static',
  trailingSlash: 'never',

  // Integrations
  integrations: [
    react({
      jsxImportSource: 'react'
    }),
    tailwind({
      applyBaseStyles: false
    }),
    sitemap({
      changefreq: 'weekly',
      priority: 0.7,
      i18n: {
        defaultLocale: 'en',
        locales: ['en', 'es', 'fr', 'de', 'pt', 'ru']
      }
    }),
    imagetools({
      defaultDirectives: (url) => {
        // Optimize images by default
        return new URLSearchParams({
          format: 'webp',
          quality: '80'
        });
      }
    })
  ],

  // Site configuration
  site: 'https://your-site.pages.dev',
  base: '/',

  // Markdown options
  markdown: {
    shikiConfig: {
      theme: 'github-dark',
      langs: ['javascript', 'typescript', 'jsx', 'tsx', 'css', 'html', 'json', 'bash']
    },
    remarkPlugins: [
      'remark-gfm',
      'remark-toc'
    ],
    rehypePlugins: [
      'rehype-slug',
      'rehype-autolink-headings'
    ]
  },

  // Vite configuration
  vite: {
    build: {
      minify: 'terser',
      sourcemap: true,
      rollupOptions: {
        output: {
          manualChunks: {
            vendor: ['react', 'react-dom'],
            astro: ['astro']
          }
        }
      }
    },
    optimizeDeps: {
      exclude: ['@astroimage/tailwindcss']
    }
  },

  // Experimental features
  experimental: {
    assets: true,
    buildElapsed: true
  }
});

// src/pages/index.astro - Homepage
---
import Layout from '../layouts/Layout.astro';
import Hero from '../components/Hero.astro';
import Features from '../components/Features.astro';
import RecentPosts from '../components/RecentPosts.astro';
import Newsletter from '../components/Newsletter.astro';

const pageTitle = 'Welcome to My Blog';
const pageDescription = 'A modern blog built with Astro and Cloudflare Pages';

// Get recent posts
const recentPosts = await Astro.glob('../content/blog/*.md');
const sortedPosts = recentPosts
  .filter(post => post.frontmatter.published)
  .sort((a, b) => new Date(b.frontmatter.pubDate) - new Date(a.frontmatter.pubDate))
  .slice(0, 3);
---

<Layout title={pageTitle} description={pageDescription}>
  <main>
    <Hero />
    <Features />
    <RecentPosts posts={sortedPosts} />
    <Newsletter />
  </main>
</Layout>

<style>
  main {
    min-height: 100vh;
  }

  /* Ensure smooth scrolling */
  html {
    scroll-behavior: smooth;
  }

  /* Custom scrollbar */
  ::-webkit-scrollbar {
    width: 8px;
  }

  ::-webkit-scrollbar-track {
    background: #f1f1f1;
  }

  ::-webkit-scrollbar-thumb {
    background: #888;
    border-radius: 4px;
  }

  ::-webkit-scrollbar-thumb:hover {
    background: #555;
  }
</style>

// src/layouts/Layout.astro - Main layout
---
export interface Props {
  title: string;
  description?: string;
  image?: string;
  noIndex?: boolean;
}

const {
  title,
  description = 'A modern blog built with Astro and Cloudflare Pages',
  image = '/images/og-default.jpg',
  noIndex = false
}: Props = Astro.props;

// Get site data
const { site } = Astro.config;
const canonicalURL = new URL(Astro.url.pathname, Astro.site);

// Social media
const socialLinks = [
  { name: 'Twitter', href: 'https://twitter.com/yourusername', icon: '🐦' },
  { name: 'GitHub', href: 'https://github.com/yourusername', icon: '🐙' },
  { name: 'LinkedIn', href: 'https://linkedin.com/in/yourusername', icon: '💼' }
];

// Navigation
const navigation = [
  { name: 'Home', href: '/' },
  { name: 'Blog', href: '/blog' },
  { name: 'About', href: '/about' },
  { name: 'Contact', href: '/contact' }
];
---

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />

    <!-- SEO Meta -->
    <title>{title}</title>
    <meta name="description" content={description} />
    <link rel="canonical" href={canonicalURL} />
    {noIndex && <meta name="robots" content="noindex, nofollow" />}

    <!-- Open Graph -->
    <meta property="og:title" content={title} />
    <meta property="og:description" content={description} />
    <meta property="og:image" content={new URL(image, Astro.site)} />
    <meta property="og:type" content="website" />
    <meta property="og:url" content={canonicalURL} />

    <!-- Twitter Card -->
    <meta name="twitter:card" content="summary_large_image" />
    <meta name="twitter:title" content={title} />
    <meta name="twitter:description" content={description} />
    <meta name="twitter:image" content={new URL(image, Astro.site)} />

    <!-- Favicon -->
    <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
    <link rel="icon" type="image/png" href="/favicon.png" />
    <link rel="apple-touch-icon" href="/apple-touch-icon.png" />

    <!-- Preconnect -->
    <link rel="preconnect" href="https://fonts.googleapis.com" />
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />

    <!-- Fonts -->
    <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet" />

    <!-- Tailwind CSS -->
    <style>
      :root {
        --font-family: 'Inter', system-ui, sans-serif;
      }
    </style>
  </head>

  <body class="bg-gray-50 text-gray-900 antialiased">
    <!-- Skip to content link -->
    <a href="#main" class="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 bg-blue-600 text-white px-4 py-2 rounded-md">
      Skip to content
    </a>

    <!-- Navigation -->
    <header class="bg-white shadow-sm border-b border-gray-200 sticky top-0 z-50">
      <nav class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
        <div class="flex justify-between items-center h-16">
          <!-- Logo -->
          <div class="flex-shrink-0">
            <a href="/" class="flex items-center space-x-2">
              <div class="w-8 h-8 bg-gradient-to-r from-blue-600 to-purple-600 rounded-lg flex items-center justify-center">
                <span class="text-white font-bold text-sm">M</span>
              </div>
              <span class="text-xl font-bold text-gray-900">MyBlog</span>
            </a>
          </div>

          <!-- Desktop Navigation -->
          <div class="hidden md:flex items-center space-x-8">
            {navigation.map(item => (
              <a
                href={item.href}
                class="text-gray-600 hover:text-gray-900 px-3 py-2 text-sm font-medium transition-colors duration-200"
              >
                {item.name}
              </a>
            ))}
          </div>

          <!-- Social Links -->
          <div class="flex items-center space-x-4">
            {socialLinks.map(link => (
              <a
                href={link.href}
                target="_blank"
                rel="noopener noreferrer"
                class="text-gray-400 hover:text-gray-600 transition-colors duration-200"
                aria-label={link.name}
              >
                <span class="text-xl">{link.icon}</span>
              </a>
            ))}

            <!-- Mobile menu button -->
            <button
              type="button"
              class="md:hidden p-2 rounded-md text-gray-400 hover:text-gray-600 hover:bg-gray-100"
              aria-label="Toggle menu"
              id="mobile-menu-button"
            >
              <svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                <path stroke-linecap="round" stroke-linejoin="round" stroke-width={2} d="M4 6h16M4 12h16M4 18h16" />
              </svg>
            </button>
          </div>
        </div>

        <!-- Mobile Navigation -->
        <div class="hidden md:hidden" id="mobile-menu">
          <div class="px-2 pt-2 pb-3 space-y-1 bg-white border-t border-gray-200">
            {navigation.map(item => (
              <a
                href={item.href}
                class="block px-3 py-2 rounded-md text-base font-medium text-gray-600 hover:text-gray-900 hover:bg-gray-50"
              >
                {item.name}
              </a>
            ))}
          </div>
        </div>
      </nav>
    </header>

    <!-- Main Content -->
    <main id="main">
      <slot />
    </main>

    <!-- Footer -->
    <footer class="bg-gray-900 text-white">
      <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
        <div class="grid grid-cols-1 md:grid-cols-4 gap-8">
          <!-- Company Info -->
          <div class="col-span-1 md:col-span-2">
            <div class="flex items-center space-x-2 mb-4">
              <div class="w-8 h-8 bg-gradient-to-r from-blue-600 to-purple-600 rounded-lg flex items-center justify-center">
                <span class="text-white font-bold text-sm">M</span>
              </div>
              <span class="text-xl font-bold">MyBlog</span>
            </div>
            <p class="text-gray-300 max-w-md">
              A modern blog platform built with Astro and deployed on Cloudflare Pages.
              Fast, secure, and sustainable web hosting at the edge.
            </p>
          </div>

          <!-- Quick Links -->
          <div>
            <h3 class="text-sm font-semibold text-gray-400 uppercase tracking-wider mb-4">
              Quick Links
            </h3>
            <ul class="space-y-2">
              {navigation.map(item => (
                <li>
                  <a href={item.href} class="text-gray-300 hover:text-white transition-colors">
                    {item.name}
                  </a>
                </li>
              ))}
            </ul>
          </div>

          <!-- Legal -->
          <div>
            <h3 class="text-sm font-semibold text-gray-400 uppercase tracking-wider mb-4">
              Legal
            </h3>
            <ul class="space-y-2">
              <li>
                <a href="/privacy" class="text-gray-300 hover:text-white transition-colors">
                  Privacy Policy
                </a>
              </li>
              <li>
                <a href="/terms" class="text-gray-300 hover:text-white transition-colors">
                  Terms of Service
                </a>
              </li>
            </ul>
          </div>
        </div>

        <!-- Bottom bar -->
        <div class="mt-8 pt-8 border-t border-gray-800">
          <div class="flex flex-col md:flex-row justify-between items-center">
            <p class="text-gray-400 text-sm">
              © {new Date().getFullYear()} MyBlog. All rights reserved.
            </p>
            <p class="text-gray-400 text-sm mt-2 md:mt-0">
              Powered by <a href="https://astro.build" target="_blank" class="hover:text-white transition-colors">Astro</a> &
              <a href="https://pages.cloudflare.com" target="_blank" class="hover:text-white transition-colors">Cloudflare Pages</a>
            </p>
          </div>
        </div>
      </div>
    </footer>

    <!-- Scripts -->
    <script>
      // Mobile menu toggle
      const mobileMenuButton = document.getElementById('mobile-menu-button');
      const mobileMenu = document.getElementById('mobile-menu');

      if (mobileMenuButton && mobileMenu) {
        mobileMenuButton.addEventListener('click', () => {
          mobileMenu.classList.toggle('hidden');
        });

        // Close menu when clicking outside
        document.addEventListener('click', (event) => {
          if (!mobileMenuButton.contains(event.target) && !mobileMenu.contains(event.target)) {
            mobileMenu.classList.add('hidden');
          }
        });
      }

      // Theme toggle (optional)
      const initTheme = () => {
        const savedTheme = localStorage.getItem('theme');
        const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;

        if (savedTheme === 'dark' || (!savedTheme && prefersDark)) {
          document.documentElement.classList.add('dark');
        }
      };

      initTheme();

      // Service Worker registration for PWA
      if ('serviceWorker' in navigator) {
        navigator.serviceWorker.register('/sw.js')
          .then(registration => {
            console.log('SW registered: ', registration);
          })
          .catch(registrationError => {
            console.log('SW registration failed: ', registrationError);
          });
      }
    </script>
  </body>
</html>

// src/pages/blog/[...slug].astro - Dynamic blog routes
---
import Layout from '../../layouts/Layout.astro';
import { getCollection } from 'astro:content';
import type { CollectionEntry } from 'astro:content';

export async function getStaticPaths() {
  const posts = await getCollection('blog');
  return posts.map(post => ({
    params: { slug: post.slug },
    props: { post }
  }));
}

const { post }: { post: CollectionEntry<'blog'> } = Astro.props;

// Get related posts
const relatedPosts = await getCollection('blog', ({ slug }) => slug !== post.slug);
const filteredRelated = relatedPosts
  .filter(relatedPost => {
    // Find posts with similar tags
    const commonTags = post.data.tags?.filter(tag =>
      relatedPost.data.tags?.includes(tag)
    );
    return (commonTags?.length || 0) > 0;
  })
  .slice(0, 3);

// Table of contents
const { Content, headings } = await post.render();
---

<Layout
  title={post.data.title}
  description={post.data.description}
  image={post.data.image}
>
  <article class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
    <!-- Article Header -->
    <header class="mb-12">
      <div class="flex items-center space-x-4 text-sm text-gray-500 mb-4">
        <time datetime={post.data.pubDate}>
          {new Date(post.data.pubDate).toLocaleDateString('en-US', {
            year: 'numeric',
            month: 'long',
            day: 'numeric'
          })}
        </time>
        <span>•</span>
        <span>{Math.ceil(post.data.readingTime || 5)} min read</span>
        {post.data.updatedDate && (
          <>
            <span>•</span>
            <span>Updated {new Date(post.data.updatedDate).toLocaleDateString()}</span>
          </>
        )}
      </div>

      <h1 class="text-4xl font-bold text-gray-900 mb-6 leading-tight">
        {post.data.title}
      </h1>

      {post.data.description && (
        <p class="text-xl text-gray-600 mb-8 leading-relaxed">
          {post.data.description}
        </p>
      )}

      {/* Author and tags */}
      <div class="flex items-center justify-between flex-wrap gap-4">
        <div class="flex items-center space-x-3">
          {post.data.authorImage && (
            <img
              src={post.data.authorImage}
              alt={post.data.author}
              class="w-12 h-12 rounded-full object-cover"
            />
          )}
          <div>
            <p class="font-medium text-gray-900">{post.data.author}</p>
            <p class="text-sm text-gray-500">{post.data.authorTitle || 'Author'}</p>
          </div>
        </div>

        {post.data.tags && post.data.tags.length > 0 && (
          <div class="flex flex-wrap gap-2">
            {post.data.tags.map(tag => (
              <span class="px-3 py-1 bg-blue-100 text-blue-800 text-xs font-medium rounded-full">
                {tag}
              </span>
            ))}
          </div>
        )}
      </div>
    </header>

    {/* Table of Contents */}
    {headings.length > 0 && (
      <aside class="mb-12 p-6 bg-gray-50 rounded-lg">
        <h2 class="text-lg font-semibold text-gray-900 mb-4">Table of Contents</h2>
        <nav>
          <ul class="space-y-2">
            {headings.map(heading => (
              <li>
                <a
                  href={`#${heading.slug}`}
                  class="text-gray-600 hover:text-gray-900 text-sm transition-colors"
                  style={`padding-left: ${(heading.depth - 1) * 16}px`}
                >
                  {heading.text}
                </a>
              </li>
            ))}
          </ul>
        </nav>
      </aside>
    )}

    {/* Article Content */}
    <div class="prose prose-lg max-w-none prose-headings:scroll-mt-20">
      <Content />
    </div>

    {/* Article Footer */}
    <footer class="mt-16 pt-8 border-t border-gray-200">
      <div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
        <div class="text-sm text-gray-500">
          Published on {new Date(post.data.pubDate).toLocaleDateString('en-US', {
            year: 'numeric',
            month: 'long',
            day: 'numeric'
          })}
        </div>

        {/* Share buttons */}
        <div class="flex items-center space-x-4">
          <span class="text-sm text-gray-500">Share:</span>
          {[
            {
              name: 'Twitter',
              href: `https://twitter.com/intent/tweet?text=${encodeURIComponent(post.data.title)}&url=${encodeURIComponent(Astro.url.pathname)}`,
              icon: '🐦'
            },
            {
              name: 'LinkedIn',
              href: `https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(Astro.url.pathname)}`,
              icon: '💼'
            },
            {
              name: 'Copy Link',
              href: '#',
              icon: '🔗',
              action: 'copy'
            }
          ].map(social => (
            <a
              href={social.href}
              target={social.action !== 'copy' ? '_blank' : undefined}
              rel={social.action !== 'copy' ? 'noopener noreferrer' : undefined}
              class="text-gray-400 hover:text-gray-600 transition-colors"
              onClick={social.action === 'copy' ? 'navigator.clipboard.writeText(window.location.href); return false;' : undefined}
              aria-label={social.name}
            >
              <span class="text-lg">{social.icon}</span>
            </a>
          ))}
        </div>
      </div>
    </footer>

    {/* Related Posts */}
    {filteredRelated.length > 0 && (
      <section class="mt-16">
        <h2 class="text-2xl font-bold text-gray-900 mb-8">Related Posts</h2>
        <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
          {filteredRelated.map(relatedPost => (
            <article class="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden hover:shadow-md transition-shadow duration-200">
              {relatedPost.data.image && (
                <a href={`/blog/${relatedPost.slug}`} class="block aspect-video overflow-hidden">
                  <img
                    src={relatedPost.data.image}
                    alt={relatedPost.data.title}
                    class="w-full h-full object-cover hover:scale-105 transition-transform duration-200"
                  />
                </a>
              )}

              <div class="p-6">
                <time datetime={relatedPost.data.pubDate} class="text-sm text-gray-500 block mb-2">
                  {new Date(relatedPost.data.pubDate).toLocaleDateString()}
                </time>

                <h3 class="text-lg font-semibold text-gray-900 mb-2">
                  <a href={`/blog/${relatedPost.slug}`} class="hover:text-blue-600 transition-colors">
                    {relatedPost.data.title}
                  </a>
                </h3>

                {relatedPost.data.description && (
                  <p class="text-gray-600 text-sm line-clamp-2">
                    {relatedPost.data.description}
                  </p>
                )}
              </div>
            </article>
          ))}
        </div>
      </section>
    )}
  </article>
</Layout>

<style>
  /* Custom prose styles */
  .prose h2 {
    @apply text-2xl font-bold text-gray-900 mt-12 mb-6;
  }

  .prose h3 {
    @apply text-xl font-semibold text-gray-900 mt-8 mb-4;
  }

  .prose p {
    @apply mb-6 leading-relaxed;
  }

  .prose code {
    @apply bg-gray-100 text-gray-800 px-2 py-1 rounded text-sm font-mono;
  }

  .prose pre {
    @apply bg-gray-900 text-gray-100 p-6 rounded-lg overflow-x-auto;
  }

  .prose pre code {
    @apply bg-transparent text-inherit p-0;
  }

  .prose blockquote {
    @apply border-l-4 border-blue-500 pl-6 italic text-gray-600;
  }

  .prose a {
    @apply text-blue-600 hover:text-blue-800 underline;
  }

  .prose ul, .prose ol {
    @apply mb-6 pl-8;
  }

  .prose li {
    @apply mb-2;
  }

  /* Line clamp utility */
  .line-clamp-2 {
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
  }
</style>

// public/sw.js - Service Worker for PWA
const CACHE_NAME = 'myblog-v1';
const STATIC_ASSETS = [
  '/',
  '/about',
  '/contact',
  '/styles/global.css',
  '/scripts/main.js',
  '/images/logo.png',
  '/favicon.ico'
];

// Install event
self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then(cache => {
        return cache.addAll(STATIC_ASSETS);
      })
  );
});

// Activate event
self.addEventListener('activate', (event) => {
  event.waitUntil(
    caches.keys()
      .then(cacheNames => {
        return Promise.all(
          cacheNames
            .filter(name => name !== CACHE_NAME)
            .map(name => caches.delete(name))
        );
      })
  );
});

// Fetch event
self.addEventListener('fetch', (event) => {
  const { request } = event;

  // Skip non-GET requests
  if (request.method !== 'GET') return;

  // Try cache first
  event.respondWith(
    caches.match(request)
      .then(cachedResponse => {
        // Return cached version if found
        if (cachedResponse) {
          return cachedResponse;
        }

        // Otherwise fetch from network
        return fetch(request)
          .then(response => {
            // Don't cache non-successful responses
            if (!response || response.status !== 200 || response.type !== 'basic') {
              return response;
            }

            // Clone response for caching
            const responseToCache = response.clone();

            caches.open(CACHE_NAME)
              .then(cache => {
                cache.put(request, responseToCache);
              });

            return response;
          })
          .catch(() => {
            // Return offline page for navigation requests
            if (request.mode === 'navigate') {
              return caches.match('/offline.html');
            }
          });
      })
  );
});