[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 - **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 принцип применен - убрали избыточность, оставили суть.
This commit is contained in:
@@ -51,6 +51,10 @@ REQUEST_TIMEOUT_SECONDS=300
|
||||
# Upload protection (optional, defaults to 10 uploads per minute per IP)
|
||||
# Simple protection against upload abuse for user-facing endpoints
|
||||
UPLOAD_LIMIT_PER_MINUTE=10
|
||||
|
||||
# Redis configuration (optional - app works without Redis)
|
||||
# If Redis is unavailable, app runs in fallback mode with warnings
|
||||
REDIS_URL=redis://localhost:6379
|
||||
```
|
||||
|
||||
## 🐳 Docker
|
||||
|
||||
@@ -175,9 +175,6 @@ RUST_LOG=info cargo run
|
||||
|
||||
### Проверка endpoints
|
||||
```bash
|
||||
# Health check
|
||||
curl http://localhost:8080/health
|
||||
|
||||
# User info (требует токен)
|
||||
curl -H "Authorization: Bearer $TOKEN" http://localhost:8080/
|
||||
|
||||
|
||||
@@ -19,8 +19,8 @@ Simple file upload/download proxy with user quotas and S3 storage.
|
||||
### 🌐 File Serving
|
||||
- **Direct file access** via filename
|
||||
- **Fast response** optimized for Vercel Edge caching
|
||||
- **CORS whitelist** for secure access
|
||||
- **Direct file serving** optimized for CDN caching
|
||||
- **CORS whitelist** for secure access (includes Vercel domains)
|
||||
- **Vercel-compatible headers** for optimal edge caching
|
||||
|
||||
## 🚀 Modern Architecture
|
||||
|
||||
|
||||
@@ -1,243 +0,0 @@
|
||||
# 🔀 Hybrid Architecture: Vercel Edge + Quoter
|
||||
|
||||
## 📋 Архитектурное решение
|
||||
|
||||
Ваша спецификация описывает **идеальную гибридную архитектуру**:
|
||||
|
||||
```
|
||||
📤 Upload: Quoter (контроль + квоты)
|
||||
📥 Download: Vercel Edge API (производительность)
|
||||
🎨 Thumbnails: Vercel /api/thumb/[width]/[...path] (динамическая генерация)
|
||||
```
|
||||
|
||||
## ✅ Преимущества гибридного подхода
|
||||
|
||||
### 🎯 **Лучшее из двух миров**
|
||||
|
||||
| Компонент | Сервис | Почему именно он |
|
||||
|-----------|---------|------------------|
|
||||
| **Upload** | Quoter | Контроль квот, кастомная логика, безопасность |
|
||||
| **Download** | Vercel | Автоматический WebP/AVIF, глобальный edge |
|
||||
| **Resize** | Vercel | Ленивая генерация, auto-optimization |
|
||||
| **OG** | Vercel | Динамическая генерация, кэширование |
|
||||
|
||||
### 💰 **Экономическая эффективность**
|
||||
- **Upload costs**: Только VPS + Storj (контролируемые)
|
||||
- **Download costs**: Vercel edge (pay-per-use, но дешевле CDN)
|
||||
- **Storage costs**: Storj (~$4/TB против $20+/TB у Vercel)
|
||||
|
||||
### 🚀 **Производительность**
|
||||
- **Upload**: Direct to S3, без proxy overhead
|
||||
- **Download**: Vercel Edge (~50ms globally)
|
||||
- **Caching**: Двухуровневое (Vercel + S3)
|
||||
|
||||
## 🔧 Интеграция с текущим Quoter
|
||||
|
||||
### 1. **Обновление CORS для Vercel**
|
||||
|
||||
```rust
|
||||
// src/main.rs - добавить Vercel в allowed origins
|
||||
let cors = Cors::default()
|
||||
.allowed_origin("https://discours.io")
|
||||
.allowed_origin("https://new.discours.io")
|
||||
.allowed_origin("https://vercel.app") // для Vercel edge functions
|
||||
.allowed_origin("http://localhost:3000") // для разработки
|
||||
.allowed_methods(vec!["GET", "POST", "OPTIONS"])
|
||||
// ...
|
||||
```
|
||||
|
||||
### 2. **Добавление заголовков для Vercel Image API**
|
||||
|
||||
```rust
|
||||
// src/handlers/common.rs - добавить заголовки для Vercel
|
||||
pub fn create_vercel_compatible_response(content_type: &str, data: Vec<u8>, etag: &str) -> HttpResponse {
|
||||
HttpResponse::Ok()
|
||||
.content_type(content_type)
|
||||
.insert_header(("etag", etag))
|
||||
.insert_header(("cache-control", CACHE_CONTROL_IMMUTABLE))
|
||||
.insert_header(("access-control-allow-origin", "*"))
|
||||
.insert_header(("x-vercel-cache", "HIT")) // для оптимизации Vercel
|
||||
.body(data)
|
||||
}
|
||||
```
|
||||
|
||||
### 3. **Endpoint для проверки доступности**
|
||||
|
||||
```rust
|
||||
// src/handlers/universal.rs - добавить health check для Vercel
|
||||
async fn handle_get(
|
||||
req: HttpRequest,
|
||||
state: web::Data<AppState>,
|
||||
path: &str,
|
||||
) -> Result<HttpResponse, actix_web::Error> {
|
||||
match path {
|
||||
"/" => crate::handlers::user::get_current_user_handler(req, state).await,
|
||||
"/health" => Ok(HttpResponse::Ok().json(serde_json::json!({
|
||||
"status": "ok",
|
||||
"service": "quoter",
|
||||
"version": env!("CARGO_PKG_VERSION")
|
||||
}))),
|
||||
_ => {
|
||||
// GET /{path} - получение файла через proxy
|
||||
let path_without_slash = path.trim_start_matches('/');
|
||||
let requested_res = web::Path::from(path_without_slash.to_string());
|
||||
crate::handlers::proxy::proxy_handler(req, requested_res, state).await
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🔄 Миграционная стратегия
|
||||
|
||||
### Этап 1: Подготовка Quoter (текущий)
|
||||
- ✅ **Готово**: Upload API с квотами и безопасностью
|
||||
- ✅ **Готово**: Система миниатюр и ресайзинга
|
||||
- ✅ **Готово**: Multi-cloud storage (Storj + AWS)
|
||||
- 🔄 **Добавить**: CORS для Vercel edge functions
|
||||
- 🔄 **Добавить**: Health check endpoint
|
||||
|
||||
### Этап 2: Настройка Vercel Edge
|
||||
```javascript
|
||||
// vercel.json - конфигурация для оптимизации
|
||||
{
|
||||
"images": {
|
||||
"deviceSizes": [64, 128, 256, 320, 400, 640, 800, 1200, 1600],
|
||||
"imageSizes": [10, 40, 110],
|
||||
"remotePatterns": [
|
||||
{
|
||||
"protocol": "https",
|
||||
"hostname": "files.dscrs.site",
|
||||
"pathname": "/**"
|
||||
}
|
||||
],
|
||||
"minimumCacheTTL": 86400,
|
||||
"dangerouslyAllowSVG": false
|
||||
},
|
||||
"functions": {
|
||||
"api/og.js": {
|
||||
"maxDuration": 30
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Этап 3: Клиентская интеграция
|
||||
```typescript
|
||||
// Проверка доступности и fallback
|
||||
export const getImageService = async (): Promise<'vercel' | 'quoter'> => {
|
||||
// Vercel по умолчанию для большинства случаев
|
||||
if (typeof window === 'undefined') return 'vercel'; // SSR
|
||||
|
||||
try {
|
||||
// Проверяем доступность Vercel Image API
|
||||
const response = await fetch('/_next/image?url=' + encodeURIComponent('https://files.dscrs.site/test.jpg') + '&w=1&q=1');
|
||||
return response.ok ? 'vercel' : 'quoter';
|
||||
} catch {
|
||||
return 'quoter'; // fallback
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## 📊 Мониторинг гибридной системы
|
||||
|
||||
### Метрики Quoter (Upload)
|
||||
```log
|
||||
# Upload успешность
|
||||
INFO Upload successful: user_123 uploaded photo.jpg (2.5MB)
|
||||
INFO Quota updated: user_123 now using 45% (5.4GB/12GB)
|
||||
|
||||
# Rate limiting
|
||||
WARN Rate limit applied: IP 192.168.1.100 blocked for upload (10/5min exceeded)
|
||||
```
|
||||
|
||||
### Метрики Vercel (Download)
|
||||
```javascript
|
||||
// api/metrics.js - собираем метрики download
|
||||
export default async function handler(req) {
|
||||
const { searchParams } = new URL(req.url);
|
||||
const source = searchParams.get('source'); // 'vercel' | 'quoter'
|
||||
const filename = searchParams.get('filename');
|
||||
|
||||
// Логируем использование
|
||||
console.log(`Image served: ${filename} via ${source}`);
|
||||
|
||||
return new Response('OK');
|
||||
}
|
||||
```
|
||||
|
||||
## 🎯 Production готовность
|
||||
|
||||
### Load Testing
|
||||
```bash
|
||||
# Test upload через Quoter
|
||||
ab -n 100 -c 10 -T 'multipart/form-data; boundary=----WebKitFormBoundary' \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
https://files.dscrs.site/
|
||||
|
||||
# Test download через Vercel
|
||||
ab -n 1000 -c 50 \
|
||||
'https://discours.io/_next/image?url=https%3A//files.dscrs.site/test.jpg&w=600&q=75'
|
||||
```
|
||||
|
||||
### Failover Strategy
|
||||
```typescript
|
||||
export const getImageWithFailover = async (filename: string, width: number) => {
|
||||
const strategies = [
|
||||
() => getVercelImageUrl(`https://files.dscrs.site/${filename}`, width),
|
||||
() => getQuoterWebpUrl(filename, width),
|
||||
() => `https://files.dscrs.site/${filename}` // fallback to original
|
||||
];
|
||||
|
||||
for (const strategy of strategies) {
|
||||
try {
|
||||
const url = strategy();
|
||||
const response = await fetch(url, { method: 'HEAD' });
|
||||
if (response.ok) return url;
|
||||
} catch (error) {
|
||||
console.warn('Image strategy failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('All image strategies failed');
|
||||
};
|
||||
```
|
||||
|
||||
## 💡 Рекомендации по оптимизации
|
||||
|
||||
### 1. **Кэширование**
|
||||
- Vercel Edge: автоматическое кэширование
|
||||
- Quoter: ETag + immutable headers
|
||||
- CDN: дополнительный слой кэширования
|
||||
|
||||
### 2. **Мониторинг**
|
||||
- Sentry для error tracking
|
||||
- Vercel Analytics для performance
|
||||
- Custom metrics для quota usage
|
||||
|
||||
### 3. **Costs optimization**
|
||||
```typescript
|
||||
// Умное переключение между сервисами
|
||||
export const getCostOptimalImageUrl = (filename: string, width: number, useCase: ImageUseCase) => {
|
||||
// Для часто используемых размеров - Vercel (лучше кэш)
|
||||
if ([300, 600, 800].includes(width)) {
|
||||
return getVercelImageUrl(`https://files.dscrs.site/${filename}`, width);
|
||||
}
|
||||
|
||||
// Для редких размеров - Quoter (избегаем Vercel billing)
|
||||
return getQuoterWebpUrl(filename, width);
|
||||
};
|
||||
```
|
||||
|
||||
## ✅ Выводы
|
||||
|
||||
Ваша архитектура идеальна потому что:
|
||||
|
||||
1. **Upload остается в Quoter** - полный контроль безопасности и квот
|
||||
2. **Download через Vercel** - глобальная производительность и auto-optimization
|
||||
3. **OG через @vercel/og** - динамическая генерация без сложности
|
||||
4. **Постепенная миграция** - можно внедрять поэтапно
|
||||
5. **Fallback стратегия** - надежность через redundancy
|
||||
|
||||
💋 **Упрощение достигнуто**: убираем сложность ресайзинга из Quoter, оставляем только upload + storage, всю оптимизацию отдаем Vercel Edge.
|
||||
|
||||
Стоит ли добавить эти изменения в код Quoter для поддержки Vercel интеграции?
|
||||
@@ -1,21 +1,21 @@
|
||||
# Vercel Thumbnail Generation Integration
|
||||
# 🚀 Vercel Frontend Migration Guide
|
||||
|
||||
## 🎯 Overview
|
||||
## 📋 Overview
|
||||
|
||||
**Quoter**: Dead simple file upload/download service. Just raw files.
|
||||
**Vercel**: Smart thumbnail generation and optimization.
|
||||
**Quoter**: Simple file upload/download service (raw files only)
|
||||
**Vercel**: Smart thumbnail generation, optimization, and global CDN
|
||||
|
||||
Perfect separation of concerns! 💋
|
||||
|
||||
## 🔗 URL Patterns for Vercel
|
||||
## 🔗 URL Patterns
|
||||
|
||||
### Quoter File URLs
|
||||
### Quoter (Raw Files)
|
||||
```
|
||||
https://quoter.discours.io/image.jpg → Original file
|
||||
https://quoter.discours.io/document.pdf → Original file
|
||||
https://files.discours.io/image.jpg → Original file
|
||||
https://files.discours.io/document.pdf → Original file
|
||||
```
|
||||
|
||||
### Vercel Thumbnail URLs (SolidJS)
|
||||
### 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
|
||||
@@ -34,13 +34,37 @@ export default defineConfig({
|
||||
},
|
||||
vite: {
|
||||
define: {
|
||||
'process.env.QUOTER_URL': JSON.stringify('https://quoter.discours.io'),
|
||||
'process.env.PUBLIC_CDN_URL': JSON.stringify('https://files.discours.io'),
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### 2. Thumbnail API Route (/api/thumb/[width]/[...path].ts)
|
||||
### 2. vercel.json Configuration
|
||||
```json
|
||||
{
|
||||
"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)
|
||||
```typescript
|
||||
import { ImageResponse } from '@vercel/og';
|
||||
import type { APIRoute } from '@solidjs/start';
|
||||
@@ -48,7 +72,7 @@ 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}`;
|
||||
const quoterUrl = `https://files.discours.io/${imagePath}`;
|
||||
|
||||
// Fetch original from Quoter
|
||||
const response = await fetch(quoterUrl);
|
||||
@@ -71,56 +95,16 @@ export const GET: APIRoute = async ({ params, request }) => {
|
||||
{
|
||||
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
|
||||
## 🔧 Frontend Integration
|
||||
|
||||
### 1. Install Dependencies
|
||||
```bash
|
||||
npm install @tanstack/solid-query @solidjs/start
|
||||
npm install @tanstack/solid-query @solidjs/start @vercel/og
|
||||
```
|
||||
|
||||
### 2. Query Client Setup (app.tsx)
|
||||
@@ -165,7 +149,7 @@ export function useFileUpload() {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
const response = await fetch('https://quoter.discours.io/', {
|
||||
const response = await fetch('https://files.discours.io/', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${getAuthToken()}`,
|
||||
@@ -205,9 +189,9 @@ export function Image(props: ImageProps) {
|
||||
const thumbnailUrl = () =>
|
||||
props.width
|
||||
? `https://new.discours.io/api/thumb/${props.width}/${props.filename}`
|
||||
: `https://quoter.discours.io/${props.filename}`;
|
||||
: `https://files.discours.io/${props.filename}`;
|
||||
|
||||
const fallbackUrl = () => `https://quoter.discours.io/${props.filename}`;
|
||||
const fallbackUrl = () => `https://files.discours.io/${props.filename}`;
|
||||
|
||||
return (
|
||||
<Switch>
|
||||
@@ -248,7 +232,7 @@ export function UserQuota() {
|
||||
const query = createQuery(() => ({
|
||||
queryKey: ['user'],
|
||||
queryFn: async () => {
|
||||
const response = await fetch('https://quoter.discours.io/', {
|
||||
const response = await fetch('https://files.discours.io/', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${getAuthToken()}`,
|
||||
},
|
||||
@@ -285,12 +269,109 @@ export function UserQuota() {
|
||||
}
|
||||
```
|
||||
|
||||
## 🔧 Implementation Steps
|
||||
## 🎨 OpenGraph Integration
|
||||
|
||||
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
|
||||
### 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(
|
||||
(
|
||||
<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
|
||||
|
||||
@@ -312,46 +393,28 @@ sequenceDiagram
|
||||
Note over Vercel: Cache thumbnail at edge
|
||||
```
|
||||
|
||||
## 🎨 Advanced Vercel Features
|
||||
## 🎯 Migration Benefits
|
||||
|
||||
### 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',
|
||||
}
|
||||
);
|
||||
}
|
||||
```
|
||||
### 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
|
||||
|
||||
### Quality Optimization
|
||||
```javascript
|
||||
// Different quality for different sizes
|
||||
const quality = width <= 400 ? 75 : width <= 800 ? 85 : 95;
|
||||
### For Vercel
|
||||
- **Edge optimization**: Global CDN caching
|
||||
- **Dynamic sizing**: Any width on-demand
|
||||
- **Smart caching**: Automatic cache invalidation
|
||||
- **Format optimization**: WebP/AVIF when supported
|
||||
|
||||
return new ImageResponse(component, {
|
||||
width,
|
||||
height,
|
||||
quality,
|
||||
});
|
||||
```
|
||||
## 🔧 Environment Variables
|
||||
|
||||
## 🔗 Integration with CORS
|
||||
|
||||
Update Quoter CORS whitelist:
|
||||
```bash
|
||||
CORS_DOWNLOAD_ORIGINS=https://discours.io,https://*.discours.io,https://*.vercel.app
|
||||
# .env.local
|
||||
QUOTER_API_URL=https://files.discours.io
|
||||
QUOTER_AUTH_TOKEN=your_jwt_token_here
|
||||
```
|
||||
|
||||
This allows Vercel Edge Functions to fetch originals from Quoter.
|
||||
|
||||
## 📈 Performance Benefits
|
||||
|
||||
- **Faster uploads**: No server-side resizing in Quoter
|
||||
@@ -361,3 +424,5 @@ This allows Vercel Edge Functions to fetch originals from Quoter.
|
||||
- **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.
|
||||
@@ -1,361 +0,0 @@
|
||||
# 🖼️ Интеграция @vercel/og с Quoter Proxy
|
||||
|
||||
## 📋 Обзор
|
||||
|
||||
`@vercel/og` - это мощная библиотека для генерации динамических OpenGraph изображений на Edge Runtime. Quoter теперь поддерживает интеграцию с @vercel/og для создания красивых социальных превью.
|
||||
|
||||
## 🔄 Изменения в архитектуре
|
||||
|
||||
### Удалённая функциональность (Legacy)
|
||||
- ❌ `src/overlay.rs` - удалена встроенная логика наложения текста
|
||||
- ❌ `src/Muller-Regular.woff2` - удалён встроенный шрифт
|
||||
- ❌ `imageproc`, `ab_glyph` dependencies - удалены из Cargo.toml
|
||||
- ❌ Параметр `s=<shout_id>` в API - больше не поддерживается
|
||||
|
||||
### Новая архитектура
|
||||
- ✅ @vercel/og обрабатывает генерацию OpenGraph изображений
|
||||
- ✅ Quoter выступает как proxy для статических файлов
|
||||
- ✅ Improved caching и performance optimization
|
||||
|
||||
## 🚀 Настройка @vercel/og
|
||||
|
||||
### 1. Установка пакета
|
||||
|
||||
```bash
|
||||
npm install @vercel/og
|
||||
# или
|
||||
yarn add @vercel/og
|
||||
```
|
||||
|
||||
### 2. Базовый пример использования
|
||||
|
||||
```typescript
|
||||
import { ImageResponse } from '@vercel/og'
|
||||
|
||||
export default function handler(req: Request) {
|
||||
return new ImageResponse(
|
||||
(
|
||||
<div
|
||||
style={{
|
||||
fontSize: 128,
|
||||
background: 'white',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
textAlign: 'center',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
Hello world!
|
||||
</div>
|
||||
),
|
||||
{
|
||||
width: 1200,
|
||||
height: 600,
|
||||
},
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## 🔗 Интеграция с Quoter Proxy
|
||||
|
||||
### Сценарий использования
|
||||
1. @vercel/og генерирует динамические OpenGraph изображения
|
||||
2. Результат сохраняется через Quoter API
|
||||
3. Quoter обслуживает изображения с кэшированием и оптимизацией
|
||||
|
||||
### Настройка Endpoint для @vercel/og
|
||||
|
||||
```typescript
|
||||
// pages/api/og/[...slug].ts
|
||||
import { ImageResponse } from '@vercel/og'
|
||||
import { NextRequest } from 'next/server'
|
||||
|
||||
export const config = {
|
||||
runtime: 'edge',
|
||||
}
|
||||
|
||||
export default async function handler(req: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = new URL(req.url)
|
||||
|
||||
// Получение параметров
|
||||
const title = searchParams.get('title') ?? 'Default Title'
|
||||
const description = searchParams.get('description') ?? 'Default Description'
|
||||
const imageUrl = searchParams.get('image') // URL изображения из Quoter
|
||||
|
||||
// Загрузка изображения через Quoter proxy
|
||||
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',
|
||||
}}
|
||||
>
|
||||
{/* Overlay для читаемости */}
|
||||
<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,
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 📤 Сохранение сгенерированных изображений в Quoter
|
||||
|
||||
### Пример интеграции
|
||||
|
||||
```typescript
|
||||
// utils/saveToQuoter.ts
|
||||
export async function saveOgImageToQuoter(
|
||||
imageBuffer: Buffer,
|
||||
filename: string,
|
||||
token: string
|
||||
): Promise<string> {
|
||||
const formData = new FormData()
|
||||
const blob = new Blob([imageBuffer], { type: 'image/png' })
|
||||
formData.append('file', blob, filename)
|
||||
|
||||
const response = await fetch('https://quoter.staging.discours.io/', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
},
|
||||
body: formData,
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to upload to Quoter: ${response.statusText}`)
|
||||
}
|
||||
|
||||
const result = await response.text()
|
||||
return result // URL загруженного файла
|
||||
}
|
||||
|
||||
// Использование
|
||||
async function generateAndSaveOgImage(title: string, description: string, token: string) {
|
||||
// Генерация изображения через @vercel/og
|
||||
const ogResponse = await fetch(`/api/og?title=${encodeURIComponent(title)}&description=${encodeURIComponent(description)}`)
|
||||
const imageBuffer = Buffer.from(await ogResponse.arrayBuffer())
|
||||
|
||||
// Сохранение в Quoter
|
||||
const filename = `og-${Date.now()}.png`
|
||||
const quoterUrl = await saveOgImageToQuoter(imageBuffer, filename, token)
|
||||
|
||||
return quoterUrl
|
||||
}
|
||||
```
|
||||
|
||||
## 🎨 Расширенные возможности
|
||||
|
||||
### Кастомные шрифты
|
||||
|
||||
```typescript
|
||||
// Загрузка шрифтов
|
||||
const font = fetch(
|
||||
new URL('./assets/Muller-Regular.woff', import.meta.url)
|
||||
).then((res) => res.arrayBuffer())
|
||||
|
||||
// Использование в ImageResponse
|
||||
return new ImageResponse(
|
||||
<div style={{ fontFamily: 'Muller' }}>
|
||||
Custom font text
|
||||
</div>,
|
||||
{
|
||||
width: 1200,
|
||||
height: 630,
|
||||
fonts: [
|
||||
{
|
||||
name: 'Muller',
|
||||
data: await font,
|
||||
style: 'normal',
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
### Динамическая загрузка изображений из Quoter
|
||||
|
||||
```typescript
|
||||
async function loadQuoterImage(imageId: string): Promise<string> {
|
||||
const quoterUrl = `https://quoter.staging.discours.io/${imageId}`
|
||||
|
||||
try {
|
||||
const response = await fetch(quoterUrl)
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`)
|
||||
|
||||
const buffer = await response.arrayBuffer()
|
||||
return `data:image/jpeg;base64,${Buffer.from(buffer).toString('base64')}`
|
||||
} catch (error) {
|
||||
console.error('Failed to load image from Quoter:', error)
|
||||
return '' // fallback
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🔧 Конфигурация Quoter для @vercel/og
|
||||
|
||||
### Environment Variables
|
||||
|
||||
```bash
|
||||
# .env.local
|
||||
QUOTER_API_URL=https://quoter.staging.discours.io
|
||||
QUOTER_AUTH_TOKEN=your_jwt_token_here
|
||||
```
|
||||
|
||||
### Типы для TypeScript
|
||||
|
||||
```typescript
|
||||
// types/quoter.ts
|
||||
export interface QuoterUploadResponse {
|
||||
url: string
|
||||
filename: string
|
||||
size: number
|
||||
contentType: string
|
||||
}
|
||||
|
||||
export interface OgImageParams {
|
||||
title: string
|
||||
description?: string
|
||||
backgroundImage?: string
|
||||
template?: 'default' | 'article' | 'profile'
|
||||
}
|
||||
```
|
||||
|
||||
## 📊 Performance & Caching
|
||||
|
||||
### Оптимизация производительности
|
||||
- ✅ @vercel/og работает на Edge Runtime
|
||||
- ✅ Quoter обеспечивает кэширование с ETag
|
||||
- ✅ Автоматическое сжатие изображений
|
||||
- ✅ CDN-дружественные HTTP заголовки
|
||||
|
||||
### Рекомендации по кэшированию
|
||||
|
||||
```typescript
|
||||
// Добавление cache headers для OG изображений
|
||||
export default function handler(req: NextRequest) {
|
||||
const imageResponse = new ImageResponse(/* ... */)
|
||||
|
||||
// Кэширование на 1 день
|
||||
imageResponse.headers.set('Cache-Control', 'public, max-age=86400, s-maxage=86400')
|
||||
imageResponse.headers.set('CDN-Cache-Control', 'public, max-age=86400')
|
||||
|
||||
return imageResponse
|
||||
}
|
||||
```
|
||||
|
||||
## 🚀 Deployment
|
||||
|
||||
### Vercel
|
||||
1. Deploy @vercel/og endpoints на Vercel
|
||||
2. Configure Quoter URL в environment variables
|
||||
3. Set up proper CORS если нужно
|
||||
|
||||
### Standalone
|
||||
1. Use Next.js standalone mode
|
||||
2. Configure Quoter integration
|
||||
3. Deploy где угодно с Node.js support
|
||||
|
||||
## 🔍 Troubleshooting
|
||||
|
||||
### Распространённые проблемы
|
||||
|
||||
1. **"Failed to load image from Quoter"**
|
||||
- Проверьте доступность Quoter API
|
||||
- Убедитесь в правильности токена авторизации
|
||||
|
||||
2. **"Font loading failed"**
|
||||
- Убедитесь, что шрифты доступны в build time
|
||||
- Используйте правильные MIME types
|
||||
|
||||
3. **"Image generation timeout"**
|
||||
- Оптимизируйте сложность layout
|
||||
- Уменьшите размер внешних ресурсов
|
||||
|
||||
## 📚 Дополнительные ресурсы
|
||||
|
||||
- [Vercel OG Documentation](https://vercel.com/docs/concepts/functions/edge-functions/og-image-generation)
|
||||
- [Quoter API Reference](./api-reference.md)
|
||||
- [Performance Best Practices](./monitoring.md)
|
||||
|
||||
---
|
||||
|
||||
💋 **Упрощение через разделение ответственности**: @vercel/og занимается генерацией, Quoter - хранением и доставкой.
|
||||
Reference in New Issue
Block a user