import type { IncomingMessage, ServerResponse } from 'http'; import { readFileSync } from 'fs'; import { join } from 'path'; type VercelRequest = IncomingMessage & { query: { [key: string]: string | string[] }; cookies: { [key: string]: string }; body: any; }; type VercelResponse = ServerResponse & { status: (code: number) => VercelResponse; json: (data: any) => VercelResponse; send: (body: string) => VercelResponse; }; import { detectBot } from './botDetection/index.js'; interface PageData { title: string; description: string; image: string; url: string; type: string; } async function getPageData(pathname: string, fullUrl: string): Promise { const normalizedPath = pathname.replace(/\/+$/, '') || '/'; // Individual park page: /parks/{slug} if (normalizedPath.startsWith('/parks/') && normalizedPath.split('/').length === 3) { const slug = normalizedPath.split('/')[2]; try { const response = await fetch( `${process.env.SUPABASE_URL}/rest/v1/parks?slug=eq.${slug}&select=name,description,banner_image_id,banner_image_url`, { headers: { 'apikey': process.env.SUPABASE_ANON_KEY!, 'Authorization': `Bearer ${process.env.SUPABASE_ANON_KEY}` } } ); if (response.ok) { const data = await response.json(); if (data && data.length > 0) { const park = data[0]; const imageUrl = park.banner_image_url || (park.banner_image_id ? `https://imagedelivery.net/${process.env.CLOUDFLARE_ACCOUNT_HASH}/${park.banner_image_id}/original` : process.env.DEFAULT_OG_IMAGE); return { title: `${park.name} - ThrillWiki`, description: park.description || `Discover ${park.name} on ThrillWiki`, image: imageUrl, url: fullUrl, type: 'website' }; } } } catch (error) { console.error(`[SSR-OG] Error fetching park data: ${error}`); } } // Individual ride page: /parks/{park-slug}/rides/{ride-slug} if (normalizedPath.match(/^\/parks\/[^\/]+\/rides\/[^\/]+$/)) { const parts = normalizedPath.split('/'); const rideSlug = parts[4]; try { const response = await fetch( `${process.env.SUPABASE_URL}/rest/v1/rides?slug=eq.${rideSlug}&select=name,description,banner_image_id,banner_image_url`, { headers: { 'apikey': process.env.SUPABASE_ANON_KEY!, 'Authorization': `Bearer ${process.env.SUPABASE_ANON_KEY}` } } ); if (response.ok) { const data = await response.json(); if (data && data.length > 0) { const ride = data[0]; const imageUrl = ride.banner_image_url || (ride.banner_image_id ? `https://imagedelivery.net/${process.env.CLOUDFLARE_ACCOUNT_HASH}/${ride.banner_image_id}/original` : process.env.DEFAULT_OG_IMAGE); return { title: `${ride.name} - ThrillWiki`, description: ride.description || `Discover ${ride.name} on ThrillWiki`, image: imageUrl, url: fullUrl, type: 'website' }; } } } catch (error) { console.error(`[SSR-OG] Error fetching ride data: ${error}`); } } // Parks listing if (normalizedPath === '/parks' || normalizedPath === '/parks/') { return { title: 'Theme Parks - ThrillWiki', description: 'Browse theme parks and amusement parks from around the world', image: process.env.DEFAULT_OG_IMAGE || 'https://imagedelivery.net/X-2-mmiWukWxvAQQ2_o-7Q/4af6a0c6-4450-497d-772f-08da62274100/original', url: fullUrl, type: 'website' }; } // Rides listing if (normalizedPath === '/rides' || normalizedPath === '/rides/') { return { title: 'Roller Coasters & Rides - ThrillWiki', description: 'Explore roller coasters and theme park rides from around the world', image: process.env.DEFAULT_OG_IMAGE || 'https://imagedelivery.net/X-2-mmiWukWxvAQQ2_o-7Q/4af6a0c6-4450-497d-772f-08da62274100/original', url: fullUrl, type: 'website' }; } // Default fallback return { title: 'ThrillWiki - Theme Park & Roller Coaster Database', description: 'Explore theme parks and roller coasters worldwide with ThrillWiki', image: process.env.DEFAULT_OG_IMAGE || 'https://imagedelivery.net/X-2-mmiWukWxvAQQ2_o-7Q/4af6a0c6-4450-497d-772f-08da62274100/original', url: fullUrl, type: 'website' }; } function generateOGTags(pageData: PageData): string { const { title, description, image, url, type } = pageData; return ` `.trim(); } function escapeHtml(text: string): string { const map: Record = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }; return text.replace(/[&<>"']/g, m => map[m]); } function injectOGTags(html: string, ogTags: string): string { // Remove existing OG tags html = html.replace(/]*>/gi, ''); // Inject new tags before const headEndIndex = html.indexOf(''); if (headEndIndex !== -1) { return html.slice(0, headEndIndex) + ogTags + '\n' + html.slice(headEndIndex); } return html; } export default async function handler(req: VercelRequest, res: VercelResponse) { try { const userAgent = req.headers['user-agent'] || ''; const fullUrl = `https://${req.headers.host}${req.url}`; const pathname = new URL(fullUrl).pathname; // Comprehensive bot detection with headers const botDetection = detectBot(userAgent, req.headers as Record); // Enhanced logging with detection details if (botDetection.isBot) { console.log(`[SSR-OG] ✅ Bot detected: ${botDetection.platform || 'unknown'} | Confidence: ${botDetection.confidence} (${botDetection.score}%) | Method: ${botDetection.detectionMethod}`); console.log(`[SSR-OG] Path: ${req.method} ${pathname}`); console.log(`[SSR-OG] UA: ${userAgent}`); if (botDetection.metadata.signals.length > 0) { console.log(`[SSR-OG] Signals: ${botDetection.metadata.signals.slice(0, 5).join(', ')}${botDetection.metadata.signals.length > 5 ? '...' : ''}`); } } else { // Log potential false negatives if (botDetection.score > 30) { console.warn(`[SSR-OG] ⚠️ Low confidence bot (${botDetection.score}%) - not serving SSR | ${req.method} ${pathname}`); console.warn(`[SSR-OG] UA: ${userAgent}`); console.warn(`[SSR-OG] Signals: ${botDetection.metadata.signals.join(', ')}`); } else { console.log(`[SSR-OG] Regular user (score: ${botDetection.score}%) | ${req.method} ${pathname}`); } } // Read the built index.html const htmlPath = join(process.cwd(), 'dist', 'index.html'); let html = readFileSync(htmlPath, 'utf-8'); if (botDetection.isBot) { // Fetch page-specific data const pageData = await getPageData(pathname, fullUrl); console.log(`[SSR-OG] Generated OG tags: ${pageData.title}`); // Generate and inject OG tags const ogTags = generateOGTags(pageData); html = injectOGTags(html, ogTags); res.setHeader('X-Bot-Platform', botDetection.platform || 'unknown'); res.setHeader('X-Bot-Confidence', botDetection.confidence); res.setHeader('X-Bot-Score', botDetection.score.toString()); res.setHeader('X-Bot-Method', botDetection.detectionMethod); res.setHeader('X-SSR-Modified', 'true'); } res.setHeader('Content-Type', 'text/html; charset=utf-8'); res.setHeader('Cache-Control', 'public, max-age=300'); res.status(200).send(html); } catch (error) { console.error('[SSR-OG] Error:', error); // Fallback: serve original HTML try { const htmlPath = join(process.cwd(), 'dist', 'index.html'); const html = readFileSync(htmlPath, 'utf-8'); res.setHeader('Content-Type', 'text/html; charset=utf-8'); res.status(200).send(html); } catch (fallbackError) { res.status(500).send('Internal Server Error'); } } }