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
202 lines
7.1 KiB
TypeScript
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, '"').replace(/'/g, ''');
|
|
|
|
// 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}`);
|
|
});
|
|
})();
|