Skip to content

Commit 9f926b8

Browse files
committed
add fallback chain for hackathon OG images with WebP skip
1 parent 21e024d commit 9f926b8

File tree

1 file changed

+125
-68
lines changed

1 file changed

+125
-68
lines changed

app/api/og/hackathons/[id]/route.tsx

Lines changed: 125 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,108 @@ import axios from 'axios';
55

66
export 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+
8110
export 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

Comments
 (0)