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: unknown; }; type VercelResponse = ServerResponse & { status: (code: number) => VercelResponse; json: (data: unknown) => VercelResponse; send: (body: string) => VercelResponse; }; import { detectBot } from './botDetection/index.js'; import { vercelLogger } from './utils/logger.js'; interface PageData { title: string; description: string; image: string; url: string; type: string; } interface ParkData { name: string; description?: string; banner_image_id?: string; banner_image_url?: string; } interface RideData { name: string; description?: string; banner_image_id?: string; banner_image_url?: string; } async function getPageData(pathname: string, fullUrl: string): Promise { const normalizedPath = pathname.replace(/\/+$/, '') || '/'; const DEFAULT_FALLBACK_IMAGE = 'https://cdn.thrillwiki.com/images/4af6a0c6-4450-497d-772f-08da62274100/original'; // 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: unknown = await response.json(); if (Array.isArray(data) && data.length > 0) { const park = data[0] as ParkData; const imageUrl = park.banner_image_url || (park.banner_image_id ? `https://cdn.thrillwiki.com/images/${park.banner_image_id}/original` : (process.env.DEFAULT_OG_IMAGE || DEFAULT_FALLBACK_IMAGE)); return { title: `${park.name} - ThrillWiki`, description: park.description || `Discover ${park.name} on ThrillWiki`, image: imageUrl, url: fullUrl, type: 'website' }; } } } catch (error) { vercelLogger.error('Error fetching park data', { error: error instanceof Error ? error.message : String(error), slug }); } } // 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: unknown = await response.json(); if (Array.isArray(data) && data.length > 0) { const ride = data[0] as RideData; const imageUrl = ride.banner_image_url || (ride.banner_image_id ? `https://cdn.thrillwiki.com/images/${ride.banner_image_id}/original` : (process.env.DEFAULT_OG_IMAGE || DEFAULT_FALLBACK_IMAGE)); return { title: `${ride.name} - ThrillWiki`, description: ride.description || `Discover ${ride.name} on ThrillWiki`, image: imageUrl, url: fullUrl, type: 'website' }; } } } catch (error) { vercelLogger.error('Error fetching ride data', { error: error instanceof Error ? error.message : String(error), slug: rideSlug }); } } // 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://cdn.thrillwiki.com/images/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://cdn.thrillwiki.com/images/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://cdn.thrillwiki.com/images/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): Promise { 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) { vercelLogger.info('Bot detected', { platform: botDetection.platform || 'unknown', confidence: botDetection.confidence, score: botDetection.score, method: botDetection.detectionMethod, path: `${req.method} ${pathname}`, userAgent, signals: botDetection.metadata.signals.slice(0, 5) }); } else { // Log potential false negatives if (botDetection.score > 30) { vercelLogger.warn('Low confidence bot - not serving SSR', { score: botDetection.score, path: `${req.method} ${pathname}`, userAgent, signals: botDetection.metadata.signals }); } else { vercelLogger.info('Regular user request', { score: botDetection.score, path: `${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); vercelLogger.info('Generated OG tags', { title: pageData.title, pathname }); // 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) { vercelLogger.error('SSR processing failed', { error: error instanceof Error ? error.message : String(error), pathname }); // 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 { res.status(500).send('Internal Server Error'); } } }