### 🚀 Изменено - Упрощение архитектуры - **Генерация миниатюр**: Полностью удалена из Quoter, теперь управляется Vercel Edge API - **Очистка legacy кода**: Удалены все функции генерации миниатюр и сложность - **Документация**: Сокращена с 17 файлов до 7, следуя принципам KISS/DRY - **Смена фокуса**: Quoter теперь сосредоточен на upload + storage, Vercel обрабатывает миниатюры - **Логирование запросов**: Добавлена аналитика источников для оптимизации CORS whitelist - **Реализация таймаутов**: Добавлены настраиваемые таймауты для S3, Redis и внешних операций - **Упрощенная безопасность**: Удален сложный rate limiting, оставлена только необходимая защита upload - **Vercel интеграция**: Добавлена поддержка Vercel Edge API с CORS и оптимизированными заголовками - **Redis graceful fallback**: Приложение теперь работает без Redis с предупреждениями вместо паники - **Умная логика ответов**: Автоматическое определение Vercel запросов и оптимизированные заголовки - **Консолидация документации**: Объединены 4 Vercel документа в один comprehensive guide ### 📝 Обновлено - Консолидирована документация в практическую структуру: - Основной README.md с быстрым стартом - docs/SETUP.md для конфигурации и развертывания - Упрощенный features.md с фокусом на основную функциональность - docs/vercel-frontend-migration.md - единый comprehensive guide для Vercel интеграции - Добавлен акцент на Vercel по всему коду и документации - Обновлены URL patterns в документации: quoter.discours.io → files.dscrs.site ### 🗑️ Удалено - Избыточные файлы документации (api-reference, deployment, development, и т.д.) - Дублирующийся контент в нескольких документах - Излишне детальная документация для простого файлового прокси - 4 отдельных Vercel документа: vercel-thumbnails.md, vercel-integration.md, hybrid-architecture.md, vercel-og-integration.md 💋 **Упрощение**: KISS принцип применен - убрали избыточность, оставили суть.
11 KiB
11 KiB
🚀 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)
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
{
"images": {
"deviceSizes": [64, 128, 256, 320, 400, 640, 800, 1200, 1600],
"imageSizes": [10, 40, 110],
"remotePatterns": [
{
"protocol": "https",
"hostname": "quoter.discours.io",
"pathname": "/**"
}
],
"minimumCacheTTL": 86400,
"dangerouslyAllowSVG": false
},
"functions": {
"api/thumb/[width]/[...path].js": {
"maxDuration": 30
}
}
}
3. 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://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(
(
<img
src={quoterUrl}
style={{
width: width,
height: 'auto',
objectFit: 'contain',
}}
/>
),
{
width: width,
height: Math.round(width * 0.75), // 4:3 aspect ratio
},
);
};
🔧 Frontend Integration
1. Install Dependencies
npm install @tanstack/solid-query @solidjs/start @vercel/og
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://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)
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 (
<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://files.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>
);
}
🎨 OpenGraph Integration
OG Image Generation (/api/og/[...slug].ts)
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(
(
<div
style={{
background: backgroundImage
? `url(${backgroundImage})`
: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
backgroundSize: 'cover',
backgroundPosition: 'center',
width: '100%',
height: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
fontFamily: 'system-ui',
}}
>
<div
style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0, 0, 0, 0.4)',
}}
/>
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
zIndex: 1,
padding: '40px',
textAlign: 'center',
}}
>
<h1
style={{
fontSize: '72px',
fontWeight: 'bold',
color: 'white',
margin: 0,
marginBottom: '20px',
textShadow: '2px 2px 4px rgba(0, 0, 0, 0.8)',
}}
>
{title}
</h1>
<p
style={{
fontSize: '36px',
color: '#f1f5f9',
margin: 0,
textShadow: '1px 1px 2px rgba(0, 0, 0, 0.8)',
}}
>
{description}
</p>
</div>
</div>
),
{
width: 1200,
height: 630,
}
);
} catch (e: any) {
console.log(`${e.message}`);
return new Response(`Failed to generate the image`, {
status: 500,
});
}
};
📊 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
🎯 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
# .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.