import express, { type Request, Response, NextFunction } from "express"; import compression from "compression"; import { registerRoutes, findVideoByAnyId } from "./routes"; import { videoSyncService } from "./videoSync"; import { setupVite, serveStatic, log } from "./vite"; import { storage } from "./storage"; import fs from "fs"; import path from "path"; const app = express(); // Enable gzip compression for faster responses app.use(compression()); app.use(express.json()); app.use(express.urlencoded({ extended: false })); // Performance and caching middleware app.use((req, res, next) => { // Set no-cache headers only for HTML pages to prevent blank screen issues if (req.path === '/' || req.path.endsWith('.html') || !req.path.includes('.')) { res.set({ 'Cache-Control': 'no-cache, no-store, must-revalidate', 'Pragma': 'no-cache', 'Expires': '0' }); } // Cache static assets for better performance else if (req.path.includes('/assets/') || req.path.match(/\.(js|css|png|jpg|jpeg|gif|ico|svg)$/)) { res.set({ 'Cache-Control': 'public, max-age=31536000', // 1 year 'ETag': `"${Date.now()}"` // Simple ETag }); } // Cache API responses for 30 seconds else if (req.path.startsWith('/api/videos') && req.method === 'GET') { res.set({ 'Cache-Control': 'public, max-age=30', 'ETag': `"videos-${Date.now()}"` }); } next(); }); app.use((req, res, next) => { const start = Date.now(); const path = req.path; let capturedJsonResponse: Record | undefined = undefined; const originalResJson = res.json; res.json = function (bodyJson, ...args) { capturedJsonResponse = bodyJson; return originalResJson.apply(res, [bodyJson, ...args]); }; res.on("finish", () => { const duration = Date.now() - start; if (path.startsWith("/api")) { let logLine = `${req.method} ${path} ${res.statusCode} in ${duration}ms`; if (capturedJsonResponse) { logLine += ` :: ${JSON.stringify(capturedJsonResponse)}`; } if (logLine.length > 80) { logLine = logLine.slice(0, 79) + "…"; } log(logLine); } }); next(); }); (async () => { // Initialize video sync service for automatic Bunny.net updates await videoSyncService.initialize(); // Social media prerendering middleware - samo za social bot-e app.get('/video/:videoId', async (req, res, next) => { const { videoId } = req.params; const userAgent = req.get('User-Agent') || ''; // Če je to Facebook, Twitter, WhatsApp ali podoben scraper const isSocialBot = /facebookexternalhit|twitterbot|whatsapp|telegrambot|discordbot|slackbot|linkedinbot|viber/i.test(userAgent); // Če NI social bot, pustimo da React routing prevzame if (!isSocialBot) { return next(); } if (isSocialBot) { try { console.log(`🤖 Social bot detected: ${userAgent}, looking for video: ${videoId}`); // Use the same findVideoByAnyId function as in routes const video = await findVideoByAnyId(videoId); if (!video) { console.log(`❌ Video not found for social bot: ${videoId}`); return next(); // Če video ne obstaja, preusmerimo na običajno SPA } console.log(`✅ Video found for social bot: ${video.title}`); console.log(`📷 Original thumbnail: ${video.thumbnailUrl}`); // Preberemo osnovni HTML template const clientTemplate = path.resolve(import.meta.dirname, "..", "client", "index.html"); let template = await fs.promises.readFile(clientTemplate, "utf-8"); // Escape special characters v naslovih in opisih const escapeHtml = (text: string) => text.replace(/"/g, '"').replace(/'/g, '''); // Zamenimo meta oznake z video specifičnimi const baseUrl = req.protocol + '://' + req.get('host'); // Use short ID for sharing URLs const shortId = video.id.replace(/-/g, '').substring(0, 8); const videoUrl = `${baseUrl}/video/${shortId}`; // Optimize thumbnail for social media sharing - larger size and JPEG format let thumbnailUrl = video.thumbnailUrl; if (thumbnailUrl) { // Replace small WebP with larger JPEG for better social media compatibility thumbnailUrl = thumbnailUrl .replace('width=400&height=225', 'width=1200&height=630') .replace('format=webp', 'format=jpg'); } else { thumbnailUrl = `${baseUrl}/api/video-thumbnail/${video.id}`; } // Zamenjamo osnovne meta oznake template = template.replace( /.*<\/title>/, `<title>${escapeHtml(video.title)} | go4.video` ); template = template.replace( / 150) { shortDescription = shortDescription.substring(0, 147) + '...'; } template = template.replace( /]*>/, `` ); // Replace og:image:type to match JPG format template = template.replace( /]*>/, `` ); template = template.replace( /]*>/, `` ); template = template.replace( /]*>/, `` ); // Dodamo dodatne OG oznake za video z detajlnimi informacijami const duration = Math.floor(video.duration / 60) + ':' + (video.duration % 60).toString().padStart(2, '0'); const additionalMeta = ` `; template = template.replace('', `${additionalMeta}\n`); res.setHeader('Content-Type', 'text/html'); res.send(template); return; } catch (error) { console.error('Error rendering video page for social bot:', error); // If an error occurs, continue with normal SPA return next(); } } // For regular users, continue with SPA next(); }); const server = await registerRoutes(app); app.use((err: any, _req: Request, res: Response, _next: NextFunction) => { const status = err.status || err.statusCode || 500; const message = err.message || "Internal Server Error"; res.status(status).json({ message }); throw err; }); // importantly only setup vite in development and after // setting up all the other routes so the catch-all route // doesn't interfere with the other routes if (app.get("env") === "development") { await setupVite(app, server); } else { serveStatic(app); } // ALWAYS serve the app on the port specified in the environment variable PORT // Other ports are firewalled. Default to 5000 if not specified. // this serves both the API and the client. // It is the only port that is not firewalled. const port = parseInt(process.env.PORT || '5000', 10); server.listen({ port, host: "0.0.0.0", reusePort: true, }, () => { log(`serving on port ${port}`); }); })();