From 789b9fed0b7bba4708966219591d99eb1964f541 Mon Sep 17 00:00:00 2001 From: dihnometry Date: Tue, 18 Nov 2025 23:42:18 -0500 Subject: [PATCH 1/2] add custom og data for hackathons --- app/api/og/hackathons/[id]/route.tsx | 95 ++++++++++++++++++++++++++-- 1 file changed, 91 insertions(+), 4 deletions(-) diff --git a/app/api/og/hackathons/[id]/route.tsx b/app/api/og/hackathons/[id]/route.tsx index 7516f1e1005..3fe55c54ff7 100644 --- a/app/api/og/hackathons/[id]/route.tsx +++ b/app/api/og/hackathons/[id]/route.tsx @@ -1,7 +1,7 @@ import type { NextRequest } from 'next/server'; import { ImageResponse } from 'next/og'; import { loadFonts, createOGResponse } from '@/utils/og-image'; -import { getHackathon } from '@/server/services/hackathons'; +import axios from 'axios'; export const runtime = 'edge'; @@ -13,7 +13,16 @@ export async function GET( const fonts = await loadFonts(); try { - const hackathon = await getHackathon(id); + const res = await axios.get( + `${process.env.NEXTAUTH_URL}/api/hackathons/${id}`, + { + headers: { + 'Cache-Control': 'no-store', + }, + } + ); + + const hackathon = res.data || null; if (!hackathon) { return createOGResponse({ @@ -24,13 +33,91 @@ export async function GET( }); } + // If hackathon has a banner, show it as the OG image + if (hackathon.banner && hackathon.banner.trim() !== '') { + try { + // Fetch the banner image with axios + const imageResponse = await axios.get(hackathon.banner, { + responseType: 'arraybuffer', + headers: { + 'Accept': 'image/*', + }, + }); + + const imageBuffer = imageResponse.data; + + // Check if we got actual image data + if (!imageBuffer || imageBuffer.byteLength === 0) { + throw new Error('Banner image is empty'); + } + + // Convert ArrayBuffer to base64 in Edge Runtime + const base64 = btoa( + new Uint8Array(imageBuffer).reduce( + (data, byte) => data + String.fromCharCode(byte), + '' + ) + ); + const contentType = imageResponse.headers['content-type'] || 'image/png'; + const imageDataUrl = `data:${contentType};base64,${base64}`; + + return new ImageResponse( + ( +
+ {hackathon.title} +
+ ), + { + width: 1280, + height: 720, + fonts: [ + { name: 'Geist-Medium', data: fonts.medium, weight: 600 }, + { name: 'Geist-Mono', data: fonts.regular, weight: 500 }, + { name: 'Geist-Light', data: fonts.light, weight: 300 } + ], + } + ); + } catch (imageError: any) { + // If banner fails to load, silently fallback to title/description + // This is expected behavior when banner doesn't exist or fails to load + if (imageError.response?.status !== 404) { + console.warn('Failed to load banner image:', imageError.message); + } + return createOGResponse({ + title: hackathon.title, + description: hackathon.description, + path: 'hackathons', + fonts + }); + } + } + + // If no banner, show title and description return createOGResponse({ title: hackathon.title, description: hackathon.description, path: 'hackathons', fonts }); - } catch (error) { + + } catch (error: any) { + console.error('Error fetching hackathon:', error.message || error); return createOGResponse({ title: 'Hackathons', description: 'Join exciting blockchain hackathons and build the future on Avalanche', @@ -38,4 +125,4 @@ export async function GET( fonts }); } -} \ No newline at end of file +} From d84f20eec9b4c244be48abc977754b3f0d503088 Mon Sep 17 00:00:00 2001 From: dihnometry Date: Thu, 20 Nov 2025 00:43:30 -0500 Subject: [PATCH 2/2] add fallback chain for hackathon OG images with WebP skip --- app/api/og/hackathons/[id]/route.tsx | 193 +++++++++++++++++---------- 1 file changed, 125 insertions(+), 68 deletions(-) diff --git a/app/api/og/hackathons/[id]/route.tsx b/app/api/og/hackathons/[id]/route.tsx index 3fe55c54ff7..ce538d6f790 100644 --- a/app/api/og/hackathons/[id]/route.tsx +++ b/app/api/og/hackathons/[id]/route.tsx @@ -5,6 +5,108 @@ import axios from 'axios'; export const runtime = 'edge'; +// Helper function to generate OG variant URL from banner URL +function generateOGBannerUrl(bannerUrl: string): string { + try { + const url = new URL(bannerUrl); + const pathname = url.pathname; + const lastDotIndex = pathname.lastIndexOf('.'); + + if (lastDotIndex === -1) { + // No extension found, just append -og + url.pathname = pathname + '-og'; + return url.toString(); + } + + // Insert -og before the extension + const basePath = pathname.substring(0, lastDotIndex); + const extension = pathname.substring(lastDotIndex); + url.pathname = basePath + '-og' + extension; + return url.toString(); + } catch { + // If URL parsing fails, try simple string replacement + const lastDotIndex = bannerUrl.lastIndexOf('.'); + if (lastDotIndex === -1) { + return bannerUrl + '-og'; + } + return bannerUrl.substring(0, lastDotIndex) + '-og' + bannerUrl.substring(lastDotIndex); + } +} + +// Helper function to try loading an image and return ImageResponse if successful +async function tryLoadImage( + imageUrl: string, + hackathonTitle: string, + fonts: { medium: ArrayBuffer, light: ArrayBuffer, regular: ArrayBuffer } +): Promise { + try { + const imageResponse = await axios.get(imageUrl, { + responseType: 'arraybuffer', + headers: { + 'Accept': 'image/*', + }, + }); + + const imageBuffer = imageResponse.data; + + if (!imageBuffer || imageBuffer.byteLength === 0) { + return null; + } + + const contentType = imageResponse.headers['content-type'] || 'image/png'; + + // Skip WebP images as they cause issues with ImageResponse + if (contentType.includes('webp') || contentType === 'image/webp') { + return null; + } + + // Convert ArrayBuffer to base64 in Edge Runtime + const base64 = btoa( + new Uint8Array(imageBuffer).reduce( + (data, byte) => data + String.fromCharCode(byte), + '' + ) + ); + const imageDataUrl = `data:${contentType};base64,${base64}`; + + return new ImageResponse( + ( +
+ {hackathonTitle} +
+ ), + { + width: 1280, + height: 720, + fonts: [ + { name: 'Geist-Medium', data: fonts.medium, weight: 600 }, + { name: 'Geist-Mono', data: fonts.regular, weight: 500 }, + { name: 'Geist-Light', data: fonts.light, weight: 300 } + ], + } + ); + } catch (error: any) { + // Return null if image fails to load (404, network error, etc.) + return null; + } +} + export async function GET( request: NextRequest, { params }: { params: Promise<{ id: string }> } @@ -33,78 +135,33 @@ export async function GET( }); } - // If hackathon has a banner, show it as the OG image + // Try to load images in fallback order if (hackathon.banner && hackathon.banner.trim() !== '') { - try { - // Fetch the banner image with axios - const imageResponse = await axios.get(hackathon.banner, { - responseType: 'arraybuffer', - headers: { - 'Accept': 'image/*', - }, - }); + // Fallback 1: Try banner with -og suffix + const ogBannerUrl = generateOGBannerUrl(hackathon.banner); + const ogImage = await tryLoadImage(ogBannerUrl, hackathon.title, fonts); + if (ogImage) { + return ogImage; + } - const imageBuffer = imageResponse.data; - - // Check if we got actual image data - if (!imageBuffer || imageBuffer.byteLength === 0) { - throw new Error('Banner image is empty'); + // Fallback 2: Try small_banner if available + if (hackathon.small_banner && hackathon.small_banner.trim() !== '') { + const smallBannerImage = await tryLoadImage(hackathon.small_banner, hackathon.title, fonts); + if (smallBannerImage) { + return smallBannerImage; } - - // Convert ArrayBuffer to base64 in Edge Runtime - const base64 = btoa( - new Uint8Array(imageBuffer).reduce( - (data, byte) => data + String.fromCharCode(byte), - '' - ) - ); - const contentType = imageResponse.headers['content-type'] || 'image/png'; - const imageDataUrl = `data:${contentType};base64,${base64}`; + } - return new ImageResponse( - ( -
- {hackathon.title} -
- ), - { - width: 1280, - height: 720, - fonts: [ - { name: 'Geist-Medium', data: fonts.medium, weight: 600 }, - { name: 'Geist-Mono', data: fonts.regular, weight: 500 }, - { name: 'Geist-Light', data: fonts.light, weight: 300 } - ], - } - ); - } catch (imageError: any) { - // If banner fails to load, silently fallback to title/description - // This is expected behavior when banner doesn't exist or fails to load - if (imageError.response?.status !== 404) { - console.warn('Failed to load banner image:', imageError.message); - } - return createOGResponse({ - title: hackathon.title, - description: hackathon.description, - path: 'hackathons', - fonts - }); + // Fallback 3: Try original banner + const bannerImage = await tryLoadImage(hackathon.banner, hackathon.title, fonts); + if (bannerImage) { + return bannerImage; + } + } else if (hackathon.small_banner && hackathon.small_banner.trim() !== '') { + // If no banner but has small_banner, try it + const smallBannerImage = await tryLoadImage(hackathon.small_banner, hackathon.title, fonts); + if (smallBannerImage) { + return smallBannerImage; } }