diff --git a/src/app/api/generate-bracket-image/route.tsx b/src/app/api/generate-bracket-image/route.tsx index 3108322..39dc521 100644 --- a/src/app/api/generate-bracket-image/route.tsx +++ b/src/app/api/generate-bracket-image/route.tsx @@ -4,25 +4,47 @@ import type { BracketState, Matchup, SeededTeam } from "@/types"; export const runtime = "edge"; +// Image size presets - smaller sizes render faster +type ImageSize = "hd" | "fullhd" | "4k"; + +const IMAGE_SIZES: Record = { + hd: { width: 1280, height: 720 }, + fullhd: { width: 1920, height: 1080 }, + "4k": { width: 3840, height: 2160 }, +}; + interface RequestBody { bracket: BracketState; userName: string; bracketName: string; + size?: ImageSize; // Optional - defaults to "4k" for best quality } // Cache for fetched logo data URIs const logoCache = new Map(); +// Convert 500px ESPN logo URL to smaller 100px version for faster fetching +function getOptimizedLogoUrl(url: string): string { + // ESPN CDN supports different sizes: /500/, /100/, etc. + return url.replace("/500/", "/100/"); +} + async function fetchLogoAsDataUri(url: string): Promise { - if (logoCache.has(url)) { - return logoCache.get(url)!; + // Use optimized (smaller) logo URL for faster fetching + const optimizedUrl = getOptimizedLogoUrl(url); + + if (logoCache.has(optimizedUrl)) { + return logoCache.get(optimizedUrl)!; } try { - const response = await fetch(url, { + const response = await fetch(optimizedUrl, { headers: { "User-Agent": "Mozilla/5.0 (compatible; NFLBracket/1.0)", }, + // Enable Next.js edge caching - cache for 24 hours, revalidate in background + cache: "force-cache", + next: { revalidate: 86400 }, }); if (!response.ok) { @@ -34,10 +56,10 @@ async function fetchLogoAsDataUri(url: string): Promise { const contentType = response.headers.get("content-type") || "image/png"; const dataUri = `data:${contentType};base64,${base64}`; - logoCache.set(url, dataUri); + logoCache.set(optimizedUrl, dataUri); return dataUri; } catch (error) { - console.error("Failed to fetch logo:", url, error); + console.error("Failed to fetch logo:", optimizedUrl, error); // Return transparent 1x1 pixel as fallback return "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII="; } @@ -78,9 +100,7 @@ function collectLogos(bracket: BracketState): string[] { } // Pre-fetch all logos and return a map -async function prefetchLogos( - logoUrls: string[], -): Promise> { +async function prefetchLogos(logoUrls: string[]): Promise> { const entries = await Promise.all( logoUrls.map(async (url) => { const dataUri = await fetchLogoAsDataUri(url); @@ -523,114 +543,121 @@ function SuperBowlImage({ export async function POST(request: NextRequest) { try { const body: RequestBody = await request.json(); - const { bracket, userName, bracketName } = body; + const { bracket, userName, bracketName, size = "4k" } = body; - // Prefetch all logos + // Prefetch all logos in parallel const logoUrls = collectLogos(bracket); const logoMap = await prefetchLogos(logoUrls); - // 4K Image dimensions - const width = 3840; - const height = 2160; + // Get image dimensions based on requested size + const { width, height } = IMAGE_SIZES[size] || IMAGE_SIZES.fullhd; - return new ImageResponse( - ( + const imageResponse = new ImageResponse( +
+ {userName}'s Playoff Bracket +
+ {bracketName && (
- {userName}'s Playoff Bracket + {bracketName}
- {bracketName && ( -
- {bracketName} -
- )} -
+ )} +
-
- - - -
+
+ + + +
-
- bracket.build • NFL Playoffs 2025-26 -
+
+ bracket.build • NFL Playoffs 2025-26
- ), + , { width, height, }, ); + + // Add cache headers to improve subsequent request performance + // Cache for 1 hour on CDN, allow stale-while-revalidate for 24 hours + const headers = new Headers(imageResponse.headers); + headers.set( + "Cache-Control", + "public, max-age=3600, s-maxage=3600, stale-while-revalidate=86400", + ); + + return new Response(imageResponse.body, { + status: 200, + headers, + }); } catch (error) { console.error("Error generating bracket image:", error); - return new Response( - JSON.stringify({ error: "Failed to generate image" }), - { - status: 500, - headers: { "Content-Type": "application/json" }, - }, - ); + return new Response(JSON.stringify({ error: "Failed to generate image" }), { + status: 500, + headers: { "Content-Type": "application/json" }, + }); } } diff --git a/src/lib/image-generator.ts b/src/lib/image-generator.ts index dfab56e..89b3dae 100644 --- a/src/lib/image-generator.ts +++ b/src/lib/image-generator.ts @@ -1,8 +1,18 @@ import type { BracketState } from "@/types"; +// Image size presets - smaller sizes render faster +export type ImageSize = "hd" | "fullhd" | "4k"; + export interface GenerateImageOptions { userName: string; bracketName: string; + /** + * Image size preset: + * - "hd" (1280x720) - fastest, good for web sharing + * - "fullhd" (1920x1080) - balanced quality/speed + * - "4k" (3840x2160) - default, highest quality + */ + size?: ImageSize; } /** @@ -22,6 +32,7 @@ export async function generateBracketImage( bracket, userName: options.userName, bracketName: options.bracketName, + size: options.size, }), });