@@ -5,6 +5,108 @@ import axios from 'axios';
55
66export const runtime = 'edge' ;
77
8+ // Helper function to generate OG variant URL from banner URL
9+ function generateOGBannerUrl ( bannerUrl : string ) : string {
10+ try {
11+ const url = new URL ( bannerUrl ) ;
12+ const pathname = url . pathname ;
13+ const lastDotIndex = pathname . lastIndexOf ( '.' ) ;
14+
15+ if ( lastDotIndex === - 1 ) {
16+ // No extension found, just append -og
17+ url . pathname = pathname + '-og' ;
18+ return url . toString ( ) ;
19+ }
20+
21+ // Insert -og before the extension
22+ const basePath = pathname . substring ( 0 , lastDotIndex ) ;
23+ const extension = pathname . substring ( lastDotIndex ) ;
24+ url . pathname = basePath + '-og' + extension ;
25+ return url . toString ( ) ;
26+ } catch {
27+ // If URL parsing fails, try simple string replacement
28+ const lastDotIndex = bannerUrl . lastIndexOf ( '.' ) ;
29+ if ( lastDotIndex === - 1 ) {
30+ return bannerUrl + '-og' ;
31+ }
32+ return bannerUrl . substring ( 0 , lastDotIndex ) + '-og' + bannerUrl . substring ( lastDotIndex ) ;
33+ }
34+ }
35+
36+ // Helper function to try loading an image and return ImageResponse if successful
37+ async function tryLoadImage (
38+ imageUrl : string ,
39+ hackathonTitle : string ,
40+ fonts : { medium : ArrayBuffer , light : ArrayBuffer , regular : ArrayBuffer }
41+ ) : Promise < ImageResponse | null > {
42+ try {
43+ const imageResponse = await axios . get ( imageUrl , {
44+ responseType : 'arraybuffer' ,
45+ headers : {
46+ 'Accept' : 'image/*' ,
47+ } ,
48+ } ) ;
49+
50+ const imageBuffer = imageResponse . data ;
51+
52+ if ( ! imageBuffer || imageBuffer . byteLength === 0 ) {
53+ return null ;
54+ }
55+
56+ const contentType = imageResponse . headers [ 'content-type' ] || 'image/png' ;
57+
58+ // Skip WebP images as they cause issues with ImageResponse
59+ if ( contentType . includes ( 'webp' ) || contentType === 'image/webp' ) {
60+ return null ;
61+ }
62+
63+ // Convert ArrayBuffer to base64 in Edge Runtime
64+ const base64 = btoa (
65+ new Uint8Array ( imageBuffer ) . reduce (
66+ ( data , byte ) => data + String . fromCharCode ( byte ) ,
67+ ''
68+ )
69+ ) ;
70+ const imageDataUrl = `data:${ contentType } ;base64,${ base64 } ` ;
71+
72+ return new ImageResponse (
73+ (
74+ < div
75+ style = { {
76+ display : 'flex' ,
77+ height : '100%' ,
78+ width : '100%' ,
79+ position : 'relative' ,
80+ overflow : 'hidden' ,
81+ } }
82+ >
83+ < img
84+ src = { imageDataUrl }
85+ alt = { hackathonTitle }
86+ style = { {
87+ width : '100%' ,
88+ height : '100%' ,
89+ objectFit : 'cover' ,
90+ } }
91+ />
92+ </ div >
93+ ) ,
94+ {
95+ width : 1280 ,
96+ height : 720 ,
97+ fonts : [
98+ { name : 'Geist-Medium' , data : fonts . medium , weight : 600 } ,
99+ { name : 'Geist-Mono' , data : fonts . regular , weight : 500 } ,
100+ { name : 'Geist-Light' , data : fonts . light , weight : 300 }
101+ ] ,
102+ }
103+ ) ;
104+ } catch ( error : any ) {
105+ // Return null if image fails to load (404, network error, etc.)
106+ return null ;
107+ }
108+ }
109+
8110export async function GET (
9111 request : NextRequest ,
10112 { params } : { params : Promise < { id : string } > }
@@ -33,78 +135,33 @@ export async function GET(
33135 } ) ;
34136 }
35137
36- // If hackathon has a banner, show it as the OG image
138+ // Try to load images in fallback order
37139 if ( hackathon . banner && hackathon . banner . trim ( ) !== '' ) {
38- try {
39- // Fetch the banner image with axios
40- const imageResponse = await axios . get ( hackathon . banner , {
41- responseType : 'arraybuffer' ,
42- headers : {
43- 'Accept' : 'image/*' ,
44- } ,
45- } ) ;
140+ // Fallback 1: Try banner with -og suffix
141+ const ogBannerUrl = generateOGBannerUrl ( hackathon . banner ) ;
142+ const ogImage = await tryLoadImage ( ogBannerUrl , hackathon . title , fonts ) ;
143+ if ( ogImage ) {
144+ return ogImage ;
145+ }
46146
47- const imageBuffer = imageResponse . data ;
48-
49- // Check if we got actual image data
50- if ( ! imageBuffer || imageBuffer . byteLength === 0 ) {
51- throw new Error ( 'Banner image is empty' ) ;
147+ // Fallback 2: Try small_banner if available
148+ if ( hackathon . small_banner && hackathon . small_banner . trim ( ) !== '' ) {
149+ const smallBannerImage = await tryLoadImage ( hackathon . small_banner , hackathon . title , fonts ) ;
150+ if ( smallBannerImage ) {
151+ return smallBannerImage ;
52152 }
53-
54- // Convert ArrayBuffer to base64 in Edge Runtime
55- const base64 = btoa (
56- new Uint8Array ( imageBuffer ) . reduce (
57- ( data , byte ) => data + String . fromCharCode ( byte ) ,
58- ''
59- )
60- ) ;
61- const contentType = imageResponse . headers [ 'content-type' ] || 'image/png' ;
62- const imageDataUrl = `data:${ contentType } ;base64,${ base64 } ` ;
153+ }
63154
64- return new ImageResponse (
65- (
66- < div
67- style = { {
68- display : 'flex' ,
69- height : '100%' ,
70- width : '100%' ,
71- position : 'relative' ,
72- overflow : 'hidden' ,
73- } }
74- >
75- < img
76- src = { imageDataUrl }
77- alt = { hackathon . title }
78- style = { {
79- width : '100%' ,
80- height : '100%' ,
81- objectFit : 'cover' ,
82- } }
83- />
84- </ div >
85- ) ,
86- {
87- width : 1280 ,
88- height : 720 ,
89- fonts : [
90- { name : 'Geist-Medium' , data : fonts . medium , weight : 600 } ,
91- { name : 'Geist-Mono' , data : fonts . regular , weight : 500 } ,
92- { name : 'Geist-Light' , data : fonts . light , weight : 300 }
93- ] ,
94- }
95- ) ;
96- } catch ( imageError : any ) {
97- // If banner fails to load, silently fallback to title/description
98- // This is expected behavior when banner doesn't exist or fails to load
99- if ( imageError . response ?. status !== 404 ) {
100- console . warn ( 'Failed to load banner image:' , imageError . message ) ;
101- }
102- return createOGResponse ( {
103- title : hackathon . title ,
104- description : hackathon . description ,
105- path : 'hackathons' ,
106- fonts
107- } ) ;
155+ // Fallback 3: Try original banner
156+ const bannerImage = await tryLoadImage ( hackathon . banner , hackathon . title , fonts ) ;
157+ if ( bannerImage ) {
158+ return bannerImage ;
159+ }
160+ } else if ( hackathon . small_banner && hackathon . small_banner . trim ( ) !== '' ) {
161+ // If no banner but has small_banner, try it
162+ const smallBannerImage = await tryLoadImage ( hackathon . small_banner , hackathon . title , fonts ) ;
163+ if ( smallBannerImage ) {
164+ return smallBannerImage ;
108165 }
109166 }
110167
0 commit comments