videofolxtv/server/index.ts
sebastjanartic 4297267681 Enhance video metadata for better social sharing and search engine visibility
Implement detailed Open Graph and Twitter Card meta tags, including video duration, release date, and site name. Update structured data for Google search results to include comprehensive video object information. Modify the thumbnail generation process to directly resize the original thumbnail to social media dimensions (1200x630) without applying a branding overlay.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: ab9cd02a-d0b2-4288-9ceb-1964d0059648
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/8cc42625-c1f5-4e43-99bd-77f2c4dedee2/ab9cd02a-d0b2-4288-9ceb-1964d0059648/xVB32z7
2025-08-31 21:12:08 +00:00

202 lines
7.1 KiB
TypeScript

import express, { type Request, Response, NextFunction } from "express";
import { registerRoutes } 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();
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
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 - mora biti PRED setupVite
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/i.test(userAgent);
if (isSocialBot) {
try {
const video = await storage.getVideo(videoId);
if (!video) {
return next(); // Če video ne obstaja, preusmerimo na običajno SPA
}
// 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');
const videoUrl = `${baseUrl}/video/${video.id}`;
const 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)}"`
);
template = template.replace(
/<meta property="og:description" content="[^"]*"/,
`<meta property="og:description" content="${escapeHtml(video.description || `Watch ${video.title} on go4.video`)}"`
);
template = template.replace(
/<meta property="og:image" content="[^"]*"/,
`<meta property="og:image" content="${thumbnailUrl}"`
);
// Zamenjamo tudi Twitter image
template = template.replace(
/<meta name="twitter:image" content="[^"]*"/,
`<meta name="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(video.description || `Watch ${video.title} on go4.video`)}"`
);
// 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:url" content="${videoUrl}">
<meta property="og:type" content="video.other">
<meta property="og:video:duration" content="${video.duration}">
<meta property="og:site_name" content="go4.video">
<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">
<!-- Strukturirani podatki za 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);
// Če se zgodi napaka, nadaljujemo z običajno SPA
return next();
}
}
// Za običajne uporabnike nadaljujemo z 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}`);
});
})();