[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 принцип применен - убрали избыточность, оставили суть.
This commit is contained in:
363
docs/vercel-thumbnails.md
Normal file
363
docs/vercel-thumbnails.md
Normal file
@@ -0,0 +1,363 @@
|
||||
# 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(
|
||||
(
|
||||
<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
|
||||
```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 (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Router>
|
||||
{/* Your app components */}
|
||||
</Router>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 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<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)
|
||||
```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)
|
||||
```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
|
||||
|
||||
```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! 🚀
|
||||
Reference in New Issue
Block a user