videofolxtv/server/index.ts
sebastjanartic a1a282709c Improve social media sharing by adding Viber support
Update social bot detection regex in server/index.ts to include 'viber' and ensure proper Open Graph tag generation for Viber.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 401e2ec0-e00d-4f10-9d0e-60f3d479f9a5
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: b8ef5700-fa21-43bd-a5fd-a29fe152119b
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/60d372ff-2c10-46c7-b01b-10c3435136b0/401e2ec0-e00d-4f10-9d0e-60f3d479f9a5/zYSbThd
2026-01-24 13:54:45 +00:00

277 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<string, any> | 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, '&quot;').replace(/'/g, '&#39;');
// 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>/,
`<title>${escapeHtml(video.title)} | go4.video</title>`
);
template = template.replace(
/<meta name="description" content="[^"]*"/,
`<meta name="description" content="${escapeHtml(video.description || `Watch ${video.title} on go4.video`)}"`
);
// Zamenjamo Open Graph meta oznake
template = template.replace(
/<meta property="og:title" content="[^"]*"/,
`<meta property="og:title" content="${escapeHtml(video.title)}"`
);
// Truncate description to 150 characters for clean social preview
let shortDescription = video.description || `${video.title} Jetzt ansehen auf video.folx.tv`;
if (shortDescription.length > 150) {
shortDescription = shortDescription.substring(0, 147) + '...';
}
template = template.replace(
/<meta property="og:description" content="[^"]*"/,
`<meta property="og:description" content="${escapeHtml(shortDescription)}"`
);
// Replace og:image - handle both with and without id attribute
template = template.replace(
/<meta property="og:image"[^>]*>/,
`<meta property="og:image" id="og-image" content="${thumbnailUrl}">`
);
// Replace og:image:type to match JPG format
template = template.replace(
/<meta property="og:image:type" content="[^"]*"/,
`<meta property="og:image:type" content="image/jpeg"`
);
// Replace Twitter image - handle both with and without id attribute
template = template.replace(
/<meta name="twitter:image"[^>]*>/,
`<meta name="twitter:image" id="twitter-image" content="${thumbnailUrl}">`
);
template = template.replace(
/<meta name="twitter:title" content="[^"]*"/,
`<meta name="twitter:title" content="${escapeHtml(video.title)}"`
);
template = template.replace(
/<meta name="twitter:description" content="[^"]*"/,
`<meta name="twitter:description" content="${escapeHtml(shortDescription)}"`
);
// Also update URL and secure image tags that have id attributes
template = template.replace(
/<meta property="og:url"[^>]*>/,
`<meta property="og:url" id="og-url" content="${videoUrl}">`
);
template = template.replace(
/<meta property="og:image:secure_url"[^>]*>/,
`<meta property="og:image:secure_url" id="og-image-secure" content="${thumbnailUrl}">`
);
// 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 = `
<meta property="og:type" content="video.other">
<meta property="og:video:duration" content="${video.duration}">
<meta property="video:duration" content="${video.duration}">
<meta property="video:release_date" content="${video.createdAt?.toISOString()}">
<!-- Dodatni Twitter Card meta podatki -->
<meta name="twitter:player:width" content="1200">
<meta name="twitter:player:height" content="630">
<meta name="twitter:creator" content="@go4video">
<!-- Structured data for Google -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "VideoObject",
"name": "${escapeHtml(video.title)}",
"description": "${escapeHtml(video.description || `Watch ${video.title} on go4.video`)}",
"thumbnailUrl": "${thumbnailUrl}",
"uploadDate": "${video.createdAt?.toISOString()}",
"duration": "PT${Math.floor(video.duration / 60)}M${video.duration % 60}S",
"contentUrl": "${videoUrl}",
"embedUrl": "${videoUrl}",
"publisher": {
"@type": "Organization",
"name": "go4.video",
"url": "${baseUrl}"
}
}
</script>`;
template = template.replace('</head>', `${additionalMeta}\n</head>`);
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}`);
});
})();