Files
quoter/docs/vercel-og-integration.md
Untone 7497b8c426
Some checks failed
Deploy / deploy (push) Has been skipped
CI / test (push) Failing after 20s
CI / lint (push) Successful in 7m1s
build-reconfig2
2025-09-02 10:46:51 +03:00

10 KiB
Raw Blame History

🖼️ Интеграция @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. Установка пакета

npm install @vercel/og
# или
yarn add @vercel/og

2. Базовый пример использования

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

// 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

Пример интеграции

// 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
}

🎨 Расширенные возможности

Кастомные шрифты

// Загрузка шрифтов
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

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

# .env.local
QUOTER_API_URL=https://quoter.staging.discours.io
QUOTER_AUTH_TOKEN=your_jwt_token_here

Типы для 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 заголовки

Рекомендации по кэшированию

// Добавление 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 занимается генерацией, Quoter - хранением и доставкой.