/** * Dynamic OG image generator — renders social share previews. * Uses next/og (ImageResponse) to create 1200×630 cards. * Reads accent color from theme config. * * GET /api/og?title=Album+Name&subtitle=12+photos */ import { ImageResponse } from 'next/og'; import { NextRequest, NextResponse } from 'next/server'; import { getConfig } from '@/lib/config'; import { checkRateLimit, getClientIp, retryAfterSeconds } from '@/lib/rate-limit'; /** * OG image rendering runs satori + resvg per request and each unique ?title * misses every cache tier, so this is far more expensive than an image proxy * hit. It must not inherit RATE_LIMIT_RPM (default 1500) — that turns the * endpoint into an unauthenticated CPU amplifier. */ const OG_RPM = 30; export async function GET(request: NextRequest) { const ip = getClientIp(request); const { theme } = getConfig(); const { success, resetAt } = checkRateLimit(`og:${ip}`, OG_RPM); if (!success) { return new NextResponse('Too many requests', { status: 429, headers: { 'Retry-After': String(retryAfterSeconds(resetAt)), 'Cache-Control': 'no-store', }, }); } const { searchParams } = request.nextUrl; const title = (searchParams.get('title') || 'Gallery').slice(0, 200); const subtitle = (searchParams.get('subtitle') || '').slice(0, 100); return new ImageResponse(