build-reconfig2
This commit is contained in:
@@ -39,16 +39,27 @@ jobs:
|
|||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-cargo-
|
${{ runner.os }}-cargo-
|
||||||
|
|
||||||
- name: Build with memory optimization
|
- name: Build with extreme memory optimization
|
||||||
run: |
|
run: |
|
||||||
|
# Create additional swap for memory-intensive compilation
|
||||||
|
sudo fallocate -l 4G /swapfile || sudo dd if=/dev/zero of=/swapfile bs=1M count=4096
|
||||||
|
sudo chmod 600 /swapfile
|
||||||
|
sudo mkswap /swapfile
|
||||||
|
sudo swapon /swapfile
|
||||||
|
|
||||||
# Apply memory optimizations for CI environment
|
# Apply memory optimizations for CI environment
|
||||||
export CARGO_NET_GIT_FETCH_WITH_CLI=true
|
export CARGO_NET_GIT_FETCH_WITH_CLI=true
|
||||||
export CARGO_NET_RETRY=3
|
export CARGO_NET_RETRY=3
|
||||||
export CARGO_NET_TIMEOUT=60
|
export CARGO_NET_TIMEOUT=60
|
||||||
export CARGO_HTTP_TIMEOUT=60
|
export CARGO_HTTP_TIMEOUT=60
|
||||||
export RUSTC_FORCE_INCREMENTAL=0
|
export RUSTC_FORCE_INCREMENTAL=0
|
||||||
# Build with our optimized configuration
|
|
||||||
|
# Build with all optimizations
|
||||||
cargo build --verbose
|
cargo build --verbose
|
||||||
|
|
||||||
|
# Clean up swap
|
||||||
|
sudo swapoff /swapfile || true
|
||||||
|
sudo rm -f /swapfile || true
|
||||||
|
|
||||||
- name: Install cargo-llvm-cov
|
- name: Install cargo-llvm-cov
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -28,6 +28,7 @@
|
|||||||
- 🚀 **HTTP кэширование** с ETag и Cache-Control заголовками для статических файлов
|
- 🚀 **HTTP кэширование** с ETag и Cache-Control заголовками для статических файлов
|
||||||
- 🚀 **Оптимизация proxy_handler** - добавлена поддержка 304 Not Modified ответов
|
- 🚀 **Оптимизация proxy_handler** - добавлена поддержка 304 Not Modified ответов
|
||||||
- 📊 **Метрики производительности** - timing логирование для всех запросов файлов
|
- 📊 **Метрики производительности** - timing логирование для всех запросов файлов
|
||||||
|
- 📚 **@vercel/og интеграция** - полная документация по интеграции с Vercel OG библиотекой
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- 🔄 **Кардинальное изменение архитектуры GET /**: переход от GraphQL API к Redis сессиям
|
- 🔄 **Кардинальное изменение архитектуры GET /**: переход от GraphQL API к Redis сессиям
|
||||||
|
|||||||
@@ -9,6 +9,10 @@
|
|||||||
- [Конфигурация](./configuration.md) - Настройка переменных окружения
|
- [Конфигурация](./configuration.md) - Настройка переменных окружения
|
||||||
- [Мониторинг](./monitoring.md) - Логирование и мониторинг
|
- [Мониторинг](./monitoring.md) - Логирование и мониторинг
|
||||||
|
|
||||||
|
### 🆕 Интеграции
|
||||||
|
- [Vercel OG Integration](./vercel-og-integration.md) - Полное руководство по интеграции с @vercel/og
|
||||||
|
- [Vercel OG Quick Start](./vercel-og-quickstart.md) - Быстрый старт за 5 минут
|
||||||
|
|
||||||
### Технические детали
|
### Технические детали
|
||||||
- [Архитектура](./architecture.md) - Техническая архитектура системы
|
- [Архитектура](./architecture.md) - Техническая архитектура системы
|
||||||
- [База данных](./database.md) - Структура Redis и схемы данных
|
- [База данных](./database.md) - Структура Redis и схемы данных
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
|
|
||||||
Quoter предоставляет REST API для загрузки файлов, управления квотами и получения файлов с автоматической генерацией миниатюр.
|
Quoter предоставляет REST API для загрузки файлов, управления квотами и получения файлов с автоматической генерацией миниатюр.
|
||||||
|
|
||||||
|
🆕 **Интеграция с @vercel/og**: Quoter теперь оптимизирован для работы с библиотекой `@vercel/og` для генерации динамических OpenGraph изображений. Подробности см. в [Vercel OG Integration Guide](./vercel-og-integration.md).
|
||||||
|
|
||||||
## Базовый URL
|
## Базовый URL
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -92,15 +94,22 @@ Authorization: Bearer <token>
|
|||||||
### 4. Получение файлов
|
### 4. Получение файлов
|
||||||
|
|
||||||
#### GET /{filename}
|
#### GET /{filename}
|
||||||
Получает файл по имени.
|
Получает файл по имени с автоматической генерацией миниатюр.
|
||||||
|
|
||||||
**Примеры:**
|
**Примеры:**
|
||||||
```
|
```
|
||||||
GET /image.jpg
|
GET /image.jpg # Оригинальный файл
|
||||||
GET /image_300.jpg
|
GET /image_300.jpg # Миниатюра 300px ширины
|
||||||
GET /image_300.jpg/webp
|
GET /image_300.jpg/webp # Миниатюра в формате WebP
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**🚫 Удаленные параметры (Legacy):**
|
||||||
|
- `s=<shout_id>` - параметр для OpenGraph overlay больше не поддерживается
|
||||||
|
- Встроенная генерация text overlay теперь обрабатывается через `@vercel/og`
|
||||||
|
|
||||||
|
**✅ Современная альтернатива:**
|
||||||
|
Для генерации OpenGraph изображений с текстом используйте интеграцию с `@vercel/og`. См. [Vercel OG Integration Guide](./vercel-og-integration.md).
|
||||||
|
|
||||||
### 5. Управление квотами
|
### 5. Управление квотами
|
||||||
|
|
||||||
#### GET /quota
|
#### GET /quota
|
||||||
|
|||||||
@@ -19,7 +19,4 @@
|
|||||||
- Для каждого пользователя устанавливается общая квота на загрузку данных, которая составляет 5 ГБ. Перед загрузкой каждого нового файла проверяется, не превысит ли его размер текущую квоту пользователя. Если квота будет превышена, загрузка файла будет отклонена. После успешной загрузки файл и его размер регистрируются в Redis, и квота пользователя обновляется.
|
- Для каждого пользователя устанавливается общая квота на загрузку данных, которая составляет 5 ГБ. Перед загрузкой каждого нового файла проверяется, не превысит ли его размер текущую квоту пользователя. Если квота будет превышена, загрузка файла будет отклонена. После успешной загрузки файл и его размер регистрируются в Redis, и квота пользователя обновляется.
|
||||||
|
|
||||||
7. **Сохранение информации о загруженных файлах в Redis**:
|
7. **Сохранение информации о загруженных файлах в Redis**:
|
||||||
- Имя каждого загруженного файла сохраняется в Redis для отслеживания загруженных пользователем файлов. Это позволяет учитывать квоты и управлять пространством, занимаемым файлами.
|
- Имя каждого загруженного файла сохраняется в Redis для отслеживания загруженных пользователем файлов. Это позволяет учитывать квоты и управлять пространством, занимаемым файлами.
|
||||||
|
|
||||||
8. **Оверлей для shout**:
|
|
||||||
- При загрузке файла, если он является изображением, и в запросе присутствует параметр `s=<shout_id>`, то к файлу будет добавлен оверлей с данными shout.
|
|
||||||
361
docs/vercel-og-integration.md
Normal file
361
docs/vercel-og-integration.md
Normal file
@@ -0,0 +1,361 @@
|
|||||||
|
# 🖼️ Интеграция @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 - хранением и доставкой.
|
||||||
233
docs/vercel-og-quickstart.md
Normal file
233
docs/vercel-og-quickstart.md
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
# 🚀 Quick Start: @vercel/og + Quoter
|
||||||
|
|
||||||
|
## ⚡ 5-минутная настройка
|
||||||
|
|
||||||
|
### 1. Установка зависимостей
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install @vercel/og
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Создание API endpoint
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// pages/api/og.tsx (Next.js)
|
||||||
|
import { ImageResponse } from '@vercel/og'
|
||||||
|
|
||||||
|
export const config = { runtime: 'edge' }
|
||||||
|
|
||||||
|
export default function handler(req) {
|
||||||
|
const { searchParams } = new URL(req.url)
|
||||||
|
const title = searchParams.get('title') ?? 'Hello World'
|
||||||
|
|
||||||
|
return new ImageResponse(
|
||||||
|
(
|
||||||
|
<div style={{
|
||||||
|
background: 'white',
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
fontSize: 60,
|
||||||
|
fontWeight: 700,
|
||||||
|
}}>
|
||||||
|
{title}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
{ width: 1200, height: 630 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Интеграция с Quoter
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// utils/quoter.ts
|
||||||
|
export async function uploadToQuoter(imageBuffer: Buffer, filename: string, token: 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,
|
||||||
|
})
|
||||||
|
|
||||||
|
return response.text() // URL файла
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Использование
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Генерация OG изображения
|
||||||
|
const ogResponse = await fetch('/api/og?title=My%20Amazing%20Post')
|
||||||
|
const imageBuffer = Buffer.from(await ogResponse.arrayBuffer())
|
||||||
|
|
||||||
|
// Загрузка в Quoter
|
||||||
|
const quoterUrl = await uploadToQuoter(imageBuffer, 'og-image.png', userToken)
|
||||||
|
|
||||||
|
// Использование в meta tags
|
||||||
|
<meta property="og:image" content={quoterUrl} />
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎨 Расширенный пример с изображением фона
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// pages/api/og-advanced.tsx
|
||||||
|
import { ImageResponse } from '@vercel/og'
|
||||||
|
|
||||||
|
export default async function handler(req) {
|
||||||
|
const { searchParams } = new URL(req.url)
|
||||||
|
const title = searchParams.get('title')
|
||||||
|
const imageUrl = searchParams.get('image') // Quoter URL
|
||||||
|
|
||||||
|
// Загружаем изображение из Quoter
|
||||||
|
let backgroundImage = null
|
||||||
|
if (imageUrl) {
|
||||||
|
const imageResponse = await fetch(imageUrl)
|
||||||
|
const buffer = await imageResponse.arrayBuffer()
|
||||||
|
backgroundImage = `data:image/jpeg;base64,${Buffer.from(buffer).toString('base64')}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ImageResponse(
|
||||||
|
(
|
||||||
|
<div style={{
|
||||||
|
background: backgroundImage ? `url(${backgroundImage})` : '#667eea',
|
||||||
|
backgroundSize: 'cover',
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}>
|
||||||
|
{/* Затемнение для читаемости */}
|
||||||
|
<div style={{
|
||||||
|
position: 'absolute',
|
||||||
|
inset: 0,
|
||||||
|
background: 'rgba(0,0,0,0.4)',
|
||||||
|
}} />
|
||||||
|
|
||||||
|
{/* Заголовок */}
|
||||||
|
<h1 style={{
|
||||||
|
fontSize: 72,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
color: 'white',
|
||||||
|
textAlign: 'center',
|
||||||
|
textShadow: '2px 2px 4px rgba(0,0,0,0.8)',
|
||||||
|
zIndex: 1,
|
||||||
|
}}>
|
||||||
|
{title}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
{ width: 1200, height: 630 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📱 React Hook для удобства
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// hooks/useOgImage.ts
|
||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
|
export function useOgImage() {
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
|
||||||
|
const generateAndUpload = async (title: string, backgroundImage?: string) => {
|
||||||
|
setLoading(true)
|
||||||
|
try {
|
||||||
|
// Генерируем OG изображение
|
||||||
|
const ogUrl = `/api/og?title=${encodeURIComponent(title)}${
|
||||||
|
backgroundImage ? `&image=${encodeURIComponent(backgroundImage)}` : ''
|
||||||
|
}`
|
||||||
|
|
||||||
|
const response = await fetch(ogUrl)
|
||||||
|
const buffer = await response.arrayBuffer()
|
||||||
|
|
||||||
|
// Загружаем в Quoter
|
||||||
|
const formData = new FormData()
|
||||||
|
const blob = new Blob([buffer], { type: 'image/png' })
|
||||||
|
formData.append('file', blob, `og-${Date.now()}.png`)
|
||||||
|
|
||||||
|
const uploadResponse = await fetch('/api/upload-to-quoter', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
})
|
||||||
|
|
||||||
|
return await uploadResponse.text()
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { generateAndUpload, loading }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Использование в компоненте
|
||||||
|
function MyComponent() {
|
||||||
|
const { generateAndUpload, loading } = useOgImage()
|
||||||
|
|
||||||
|
const handleCreateOg = async () => {
|
||||||
|
const quoterUrl = await generateAndUpload('My Post Title', '/existing-image.jpg')
|
||||||
|
console.log('OG image uploaded to:', quoterUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button onClick={handleCreateOg} disabled={loading}>
|
||||||
|
{loading ? 'Generating...' : 'Create OG Image'}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## ⚡ Production Tips
|
||||||
|
|
||||||
|
### Кэширование
|
||||||
|
```typescript
|
||||||
|
// Cache OG images for 24 hours
|
||||||
|
export default function handler(req) {
|
||||||
|
const response = new ImageResponse(/* ... */)
|
||||||
|
|
||||||
|
response.headers.set('Cache-Control', 'public, max-age=86400')
|
||||||
|
response.headers.set('CDN-Cache-Control', 'public, max-age=86400')
|
||||||
|
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error Handling
|
||||||
|
```typescript
|
||||||
|
export default async function handler(req) {
|
||||||
|
try {
|
||||||
|
return new ImageResponse(/* ... */)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('OG generation failed:', error)
|
||||||
|
|
||||||
|
// Fallback изображение
|
||||||
|
return new Response('Failed to generate image', { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
```bash
|
||||||
|
# .env.local
|
||||||
|
QUOTER_API_URL=https://quoter.staging.discours.io
|
||||||
|
QUOTER_AUTH_TOKEN=your_jwt_token
|
||||||
|
NEXT_PUBLIC_OG_BASE_URL=https://yoursite.com/api/og
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔗 Полезные ссылки
|
||||||
|
|
||||||
|
- [Полная документация по интеграции](./vercel-og-integration.md)
|
||||||
|
- [Quoter API Reference](./api-reference.md)
|
||||||
|
- [Vercel OG Official Docs](https://vercel.com/docs/concepts/functions/edge-functions/og-image-generation)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
💋 **Упрощение**: Один endpoint для генерации, один для загрузки - минимум кода, максимум результата!
|
||||||
Reference in New Issue
Block a user