# 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) ```typescript 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) ```typescript 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( ( ), { 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 ```bash # 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 ```bash npm install @tanstack/solid-query @solidjs/start ``` ### 2. Query Client Setup (app.tsx) ```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 ( {/* Your app components */} ); } ``` ### 3. File Upload Hook (hooks/useFileUpload.ts) ```tsx 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 => { 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) ```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 ( setLoading(false)} onError={() => { setLoading(false); setLoadError(true); }} /> setLoading(false)} /> ); } ``` ### 5. User Quota Component (components/UserQuota.tsx) ```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 ( Loading quota... Error loading quota {(data) => ( Storage: {data().storage_used_mb}MB / {data().storage_limit_mb}MB )} ); } ``` ## 🔧 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 ```mermaid 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 ```javascript // 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 ```javascript // 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: ```bash 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! 🚀
Storage: {data().storage_used_mb}MB / {data().storage_limit_mb}MB