# 🚀 Vercel Frontend Migration Guide ## 📋 Overview **Quoter**: Simple file upload/download service (raw files only) **Vercel**: Smart thumbnail generation, optimization, and global CDN Perfect separation of concerns! 💋 ## 🔗 URL Patterns ### Quoter (Raw Files) ``` https://files.discours.io/image.jpg → Original file https://files.discours.io/document.pdf → Original file ``` ### Vercel (Optimized Thumbnails) ``` 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.PUBLIC_CDN_URL': JSON.stringify('https://files.discours.io'), }, }, }); ``` ### 2. vercel.json Configuration ```json { "images": { "deviceSizes": [64, 128, 256, 320, 400, 640, 800, 1200, 1600], "imageSizes": [10, 40, 110], "remotePatterns": [ { "protocol": "https", "hostname": "files.discours.io", "pathname": "/**" } ], "minimumCacheTTL": 86400, "dangerouslyAllowSVG": false }, "functions": { "api/thumb/[width]/[...path].js": { "maxDuration": 30 } } } ``` ### 3. 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://files.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 }, ); }; ``` ## 🔧 Frontend Integration ### 1. Install Dependencies ```bash npm install @tanstack/solid-query @solidjs/start @vercel/og ``` ### 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://files.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://files.discours.io/${props.filename}`; const fallbackUrl = () => `https://files.discours.io/${props.filename}`; return (
{props.alt} setLoading(false)} onError={() => { setLoading(false); setLoadError(true); }} /> {props.alt} 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://files.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

)} ); } ``` ## 🎨 OpenGraph Integration ### OG Image Generation (/api/og/[...slug].ts) ```typescript import { ImageResponse } from '@vercel/og'; import type { APIRoute } from '@solidjs/start'; export const GET: APIRoute = async ({ params, request }) => { try { const { searchParams } = new URL(request.url); const title = searchParams.get('title') ?? 'Default Title'; const description = searchParams.get('description') ?? 'Default Description'; const imageUrl = searchParams.get('image'); // URL from Quoter // Load image from Quoter if provided let backgroundImage = null; if (imageUrl) { try { const imageResponse = await fetch(imageUrl); const imageBuffer = await imageResponse.arrayBuffer(); backgroundImage = `data:image/jpeg;base64,${Buffer.from(imageBuffer).toString('base64')}`; } catch (error) { console.error('Failed to load image from Quoter:', error); } } return new ImageResponse( (

{title}

{description}

), { width: 1200, height: 630, } ); } catch (e: any) { console.log(`${e.message}`); return new Response(`Failed to generate the image`, { status: 500, }); } }; ``` ## 📊 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 ``` ## 🎯 Migration Benefits ### 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 ## 🔧 Environment Variables ```bash # .env.local QUOTER_API_URL=https://files.discours.io QUOTER_AUTH_TOKEN=your_jwt_token_here ``` ## 📈 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! 🚀 💋 **KISS & DRY**: One comprehensive guide instead of 4 separate documents.