From c60f1058de55d8749c514f402e42fa2259206562 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 11 Jan 2026 00:11:47 +0000 Subject: [PATCH 1/2] feat: Add image size options and caching to bracket generator Co-authored-by: avatarneil --- src/app/api/generate-bracket-image/route.tsx | 178 ++++++++++--------- src/lib/image-generator.ts | 11 ++ 2 files changed, 109 insertions(+), 80 deletions(-) diff --git a/src/app/api/generate-bracket-image/route.tsx b/src/app/api/generate-bracket-image/route.tsx index 3108322..fba8bb6 100644 --- a/src/app/api/generate-bracket-image/route.tsx +++ b/src/app/api/generate-bracket-image/route.tsx @@ -4,10 +4,20 @@ 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 "fullhd" for better performance } // Cache for fetched logo data URIs @@ -23,6 +33,9 @@ async function fetchLogoAsDataUri(url: string): Promise { 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) { @@ -78,9 +91,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 +534,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 = "fullhd" } = 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..485ca99 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) - default, balanced quality/speed + * - "4k" (3840x2160) - highest quality, slower to generate + */ + size?: ImageSize; } /** @@ -22,6 +32,7 @@ export async function generateBracketImage( bracket, userName: options.userName, bracketName: options.bracketName, + size: options.size || "fullhd", }), }); From 95b58579999a65eb0eadc040dfcaf70e5aa4b47e Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 11 Jan 2026 01:39:19 +0000 Subject: [PATCH 2/2] feat: Default image size to 4k and optimize logo fetching Co-authored-by: avatarneil --- src/app/api/generate-bracket-image/route.tsx | 23 ++++++++++++++------ src/lib/image-generator.ts | 6 ++--- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/src/app/api/generate-bracket-image/route.tsx b/src/app/api/generate-bracket-image/route.tsx index fba8bb6..39dc521 100644 --- a/src/app/api/generate-bracket-image/route.tsx +++ b/src/app/api/generate-bracket-image/route.tsx @@ -17,19 +17,28 @@ interface RequestBody { bracket: BracketState; userName: string; bracketName: string; - size?: ImageSize; // Optional - defaults to "fullhd" for better performance + 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)", }, @@ -47,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="; } @@ -534,7 +543,7 @@ function SuperBowlImage({ export async function POST(request: NextRequest) { try { const body: RequestBody = await request.json(); - const { bracket, userName, bracketName, size = "fullhd" } = body; + const { bracket, userName, bracketName, size = "4k" } = body; // Prefetch all logos in parallel const logoUrls = collectLogos(bracket); diff --git a/src/lib/image-generator.ts b/src/lib/image-generator.ts index 485ca99..89b3dae 100644 --- a/src/lib/image-generator.ts +++ b/src/lib/image-generator.ts @@ -9,8 +9,8 @@ export interface GenerateImageOptions { /** * Image size preset: * - "hd" (1280x720) - fastest, good for web sharing - * - "fullhd" (1920x1080) - default, balanced quality/speed - * - "4k" (3840x2160) - highest quality, slower to generate + * - "fullhd" (1920x1080) - balanced quality/speed + * - "4k" (3840x2160) - default, highest quality */ size?: ImageSize; } @@ -32,7 +32,7 @@ export async function generateBracketImage( bracket, userName: options.userName, bracketName: options.bracketName, - size: options.size || "fullhd", + size: options.size, }), });