- Create /api/og-image endpoint that generates SVG with 1.91:1 ratio (1200x630px) - Prevents logo cropping on Facebook and Twitter - Uses safe padding (150px) to ensure content is never cut off - Update default OG image to use dynamic endpoint - Add SEO testing documentation
79 lines
3.0 KiB
TypeScript
79 lines
3.0 KiB
TypeScript
import { NextResponse } from 'next/server';
|
|
import { config } from '@/lib/config';
|
|
import { getBaseUrl } from '@/lib/seo';
|
|
import { headers } from 'next/headers';
|
|
|
|
export const dynamic = 'force-dynamic';
|
|
export const runtime = 'nodejs';
|
|
|
|
/**
|
|
* Generate Open Graph image as SVG with correct aspect ratio (1.91:1 = 1200x630)
|
|
* This prevents cropping on Facebook and Twitter
|
|
*/
|
|
export async function GET() {
|
|
const baseUrl = await getBaseUrl();
|
|
const appName = config.appName;
|
|
const bgColor = config.colors.backgroundColor || '#ffffff';
|
|
const primaryColor = config.colors.themeColor || '#000000';
|
|
|
|
// SVG with correct Open Graph dimensions: 1200x630 (1.91:1 ratio)
|
|
// Safe area: 150px padding on all sides to prevent cropping
|
|
// This ensures content is never cut off on Facebook/Twitter
|
|
const svg = `<?xml version="1.0" encoding="UTF-8"?>
|
|
<svg width="1200" height="630" xmlns="http://www.w3.org/2000/svg">
|
|
<!-- Background -->
|
|
<rect width="1200" height="630" fill="${bgColor}"/>
|
|
|
|
<!-- Gradient definition -->
|
|
<defs>
|
|
<linearGradient id="gradient" x1="0%" y1="0%" x2="100%" y2="0%">
|
|
<stop offset="0%" style="stop-color:#667eea;stop-opacity:1" />
|
|
<stop offset="50%" style="stop-color:#764ba2;stop-opacity:1" />
|
|
<stop offset="100%" style="stop-color:#06b6d4;stop-opacity:1" />
|
|
</linearGradient>
|
|
</defs>
|
|
|
|
<!-- Content container - centered with safe padding (150px on all sides) -->
|
|
<g transform="translate(150, 150)">
|
|
<!-- Main graphic area (centered horizontally) -->
|
|
<g transform="translate(300, 0)">
|
|
<!-- Musical note (left side, within safe area) -->
|
|
<g fill="url(#gradient)" opacity="0.9">
|
|
<!-- Note head -->
|
|
<ellipse cx="0" cy="40" rx="40" ry="28"/>
|
|
<!-- Note stem -->
|
|
<rect x="30" y="-60" width="16" height="100" rx="2"/>
|
|
</g>
|
|
|
|
<!-- Waveform (center-right, within safe area) -->
|
|
<g transform="translate(70, 15)" fill="none" stroke="url(#gradient)" stroke-width="8" stroke-linecap="round" opacity="0.8">
|
|
<path d="M 0 25 Q 20 -15 40 25 T 80 25"/>
|
|
<path d="M 0 40 Q 20 0 40 40 T 80 40"/>
|
|
<path d="M 0 55 Q 20 15 40 55 T 80 55"/>
|
|
</g>
|
|
|
|
<!-- Vertical bar (right side, within safe area) -->
|
|
<rect x="170" y="0" width="10" height="120" fill="url(#gradient)" opacity="0.7" rx="5"/>
|
|
</g>
|
|
|
|
<!-- App name (centered, within safe vertical area) -->
|
|
<text x="450" y="180" font-family="system-ui, -apple-system, sans-serif" font-size="56" font-weight="bold" fill="${primaryColor}" text-anchor="middle" letter-spacing="-0.5">
|
|
${appName}
|
|
</text>
|
|
|
|
<!-- Domain/subtitle (centered, within safe vertical area) -->
|
|
<text x="450" y="220" font-family="system-ui, -apple-system, sans-serif" font-size="28" fill="#666666" text-anchor="middle">
|
|
${config.domain}
|
|
</text>
|
|
</g>
|
|
</svg>`;
|
|
|
|
return new NextResponse(svg, {
|
|
headers: {
|
|
'Content-Type': 'image/svg+xml',
|
|
'Cache-Control': 'public, max-age=3600, s-maxage=3600',
|
|
},
|
|
});
|
|
}
|
|
|