Files
quoter/docs/vercel-thumbnails.md
Untone 7973ba0027
Some checks failed
Deploy / deploy (push) Has been skipped
CI / lint (push) Failing after 8s
CI / test (push) Failing after 10m26s
[0.6.1] - 2025-09-02
### 🚀 Изменено - Упрощение архитектуры
- **Генерация миниатюр**: Полностью удалена из Quoter, теперь управляется Vercel Edge API
- **Очистка legacy кода**: Удалены все функции генерации миниатюр и сложность
- **Документация**: Сокращена с 17 файлов до 7, следуя принципам KISS/DRY
- **Смена фокуса**: Quoter теперь сосредоточен на upload + storage, Vercel обрабатывает миниатюры
- **Логирование запросов**: Добавлена аналитика источников для оптимизации CORS whitelist
- **Реализация таймаутов**: Добавлены настраиваемые таймауты для S3, Redis и внешних операций
- **Упрощенная безопасность**: Удален сложный rate limiting, оставлена только необходимая защита upload

### 📝 Обновлено
- Консолидирована документация в практическую структуру:
  - Основной README.md с быстрым стартом
  - docs/SETUP.md для конфигурации и развертывания
  - Упрощенный features.md с фокусом на основную функциональность
- Добавлен акцент на Vercel по всему коду и документации

### 🗑️ Удалено
- Избыточные файлы документации (api-reference, deployment, development, и т.д.)
- Дублирующийся контент в нескольких документах
- Излишне детальная документация для простого файлового прокси

💋 **Упрощение**: KISS принцип применен - убрали избыточность, оставили суть.
2025-09-02 14:00:54 +03:00

9.1 KiB

Vercel Thumbnail Generation Integration

🎯 Overview

Quoter: Dead simple file upload/download service. Just raw files.
Vercel: Smart thumbnail generation and optimization.

Perfect separation of concerns! 💋

🔗 URL Patterns for Vercel

Quoter File URLs

https://quoter.discours.io/image.jpg          → Original file
https://quoter.discours.io/document.pdf       → Original file  

Vercel Thumbnail URLs (SolidJS)

https://new.discours.io/api/thumb/300/image.jpg    → 300px width
https://new.discours.io/api/thumb/600/image.jpg    → 600px width
https://new.discours.io/api/thumb/1200/image.jpg   → 1200px width

🛠️ Vercel Configuration

1. SolidJS Start Config (app.config.ts)

import { defineConfig } from '@solidjs/start/config';

export default defineConfig({
  server: {
    preset: 'vercel',
  },
  vite: {
    define: {
      'process.env.QUOTER_URL': JSON.stringify('https://quoter.discours.io'),
    },
  },
});

2. Thumbnail API Route (/api/thumb/[width]/[...path].ts)

import { ImageResponse } from '@vercel/og';
import type { APIRoute } from '@solidjs/start';

export const GET: APIRoute = async ({ params, request }) => {
  const width = parseInt(params.width);
  const imagePath = params.path.split('/').join('/');
  const quoterUrl = `https://quoter.discours.io/${imagePath}`;
  
  // Fetch original from Quoter
  const response = await fetch(quoterUrl);
  if (!response.ok) {
    return new Response('Image not found', { status: 404 });
  }
  
  // Generate optimized thumbnail using @vercel/og
  return new ImageResponse(
    (
      <img 
        src={quoterUrl}
        style={{
          width: width,
          height: 'auto',
          objectFit: 'contain',
        }}
      />
    ),
    {
      width: width,
      height: Math.round(width * 0.75), // 4:3 aspect ratio
    }
  );
};

📋 File Naming Conventions

Quoter Storage (No Width Patterns)

✅ image.jpg                 → Clean filename
✅ photo-2024.png            → kebab-case
✅ user-avatar.webp          → descriptive names
✅ document.pdf              → any file type

❌ image_300.jpg             → No width patterns needed
❌ photo-thumbnail.jpg       → No thumbnail suffix
❌ userAvatar.png            → No camelCase

URL Routing Examples

# Client requests thumbnail
GET /api/thumb/600/image.jpg

# Vercel fetches original from Quoter  
GET https://quoter.discours.io/image.jpg

# Vercel generates and caches 600px thumbnail
→ Returns optimized image

🚀 Benefits of This Architecture

For Quoter

  • Simple storage: Just store original files
  • No processing: Zero thumbnail generation load
  • Fast uploads: Direct S3 storage without resizing
  • Predictable URLs: Clean file paths

For Vercel

  • Edge optimization: Global CDN caching
  • Dynamic sizing: Any width on-demand
  • Smart caching: Automatic cache invalidation
  • Format optimization: WebP/AVIF when supported

🔧 SolidJS Frontend Integration

1. Install Dependencies

npm install @tanstack/solid-query @solidjs/start

2. Query Client Setup (app.tsx)

import { QueryClient, QueryClientProvider } from '@tanstack/solid-query';
import { Router } from '@solidjs/router';

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 1000 * 60 * 5, // 5 minutes
      refetchOnWindowFocus: false,
    },
  },
});

export default function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <Router>
        {/* Your app components */}
      </Router>
    </QueryClientProvider>
  );
}

3. File Upload Hook (hooks/useFileUpload.ts)

import { createMutation, useQueryClient } from '@tanstack/solid-query';

interface UploadResponse {
  url: string;
  filename: string;
}

export function useFileUpload() {
  const queryClient = useQueryClient();
  
  return createMutation(() => ({
    mutationFn: async (file: File): Promise<UploadResponse> => {
      const formData = new FormData();
      formData.append('file', file);
      
      const response = await fetch('https://quoter.discours.io/', {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${getAuthToken()}`,
        },
        body: formData,
      });
      
      if (!response.ok) {
        throw new Error('Upload failed');
      }
      
      return response.json();
    },
    onSuccess: () => {
      // Invalidate user quota query
      queryClient.invalidateQueries({ queryKey: ['user'] });
    },
  }));
}

4. Image Component with Thumbnails (components/Image.tsx)

import { createSignal, Show, Switch, Match } from 'solid-js';

interface ImageProps {
  filename: string;
  width?: number;
  alt: string;
  fallback?: boolean;
}

export function Image(props: ImageProps) {
  const [loadError, setLoadError] = createSignal(false);
  const [loading, setLoading] = createSignal(true);
  
  const thumbnailUrl = () => 
    props.width 
      ? `https://new.discours.io/api/thumb/${props.width}/${props.filename}`
      : `https://quoter.discours.io/${props.filename}`;
  
  const fallbackUrl = () => `https://quoter.discours.io/${props.filename}`;
  
  return (
    <Switch>
      <Match when={loading()}>
        <div class="bg-gray-200 animate-pulse" style={{ width: `${props.width}px`, height: '200px' }} />
      </Match>
      <Match when={!loadError()}>
        <img
          src={thumbnailUrl()}
          alt={props.alt}
          loading="lazy"
          onLoad={() => setLoading(false)}
          onError={() => {
            setLoading(false);
            setLoadError(true);
          }}
        />
      </Match>
      <Match when={loadError() && props.fallback !== false}>
        <img
          src={fallbackUrl()}
          alt={props.alt}
          loading="lazy"
          onLoad={() => setLoading(false)}
        />
      </Match>
    </Switch>
  );
}

5. User Quota Component (components/UserQuota.tsx)

import { createQuery } from '@tanstack/solid-query';
import { Show, Switch, Match } from 'solid-js';

export function UserQuota() {
  const query = createQuery(() => ({
    queryKey: ['user'],
    queryFn: async () => {
      const response = await fetch('https://quoter.discours.io/', {
        headers: {
          'Authorization': `Bearer ${getAuthToken()}`,
        },
      });
      return response.json();
    },
  }));
  
  return (
    <Switch>
      <Match when={query.isLoading}>
        <div>Loading quota...</div>
      </Match>
      <Match when={query.isError}>
        <div>Error loading quota</div>
      </Match>
      <Match when={query.isSuccess}>
        <Show when={query.data}>
          {(data) => (
            <div>
              <p>Storage: {data().storage_used_mb}MB / {data().storage_limit_mb}MB</p>
              <div class="w-full bg-gray-200 rounded-full h-2">
                <div 
                  class="bg-blue-600 h-2 rounded-full" 
                  style={{ width: `${(data().storage_used_mb / data().storage_limit_mb) * 100}%` }}
                />
              </div>
            </div>
          )}
        </Show>
      </Match>
    </Switch>
  );
}

🔧 Implementation Steps

  1. Quoter: Serve raw files only (no patterns)
  2. Vercel: Create SolidJS API routes for thumbnails
  3. Frontend: Use TanStack Query for data fetching
  4. CORS: Configure Quoter to allow Vercel domain

📊 Request Flow

sequenceDiagram
    participant Client
    participant Vercel
    participant Quoter
    participant S3

    Client->>Vercel: GET /api/thumb/600/image.jpg
    Vercel->>Quoter: GET /image.jpg (original)
    Quoter->>S3: Fetch image.jpg
    S3->>Quoter: Return file data
    Quoter->>Vercel: Return original image
    Vercel->>Vercel: Generate 600px thumbnail
    Vercel->>Client: Return optimized thumbnail
    
    Note over Vercel: Cache thumbnail at edge

🎨 Advanced Vercel Features

Smart Format Detection

// Auto-serve WebP/AVIF when supported
export async function GET(request) {
  const accept = request.headers.get('accept');
  const supportsWebP = accept?.includes('image/webp');
  const supportsAVIF = accept?.includes('image/avif');
  
  return new ImageResponse(
    // ... image component
    {
      format: supportsAVIF ? 'avif' : supportsWebP ? 'webp' : 'jpeg',
    }
  );
}

Quality Optimization

// Different quality for different sizes
const quality = width <= 400 ? 75 : width <= 800 ? 85 : 95;

return new ImageResponse(component, {
  width,
  height,
  quality,
});

🔗 Integration with CORS

Update Quoter CORS whitelist:

CORS_DOWNLOAD_ORIGINS=https://discours.io,https://*.discours.io,https://*.vercel.app

This allows Vercel Edge Functions to fetch originals from Quoter.

📈 Performance Benefits

  • Faster uploads: No server-side resizing in Quoter
  • Global CDN: Vercel Edge caches thumbnails worldwide
  • On-demand sizing: Generate any size when needed
  • Smart caching: Automatic cache headers and invalidation
  • Format optimization: Serve modern formats automatically

Result: Clean separation of concerns - Quoter handles storage, Vercel handles optimization! 🚀