folx-tv/server/vite.ts
sebastjanartic 8a263dee0f Add personalized meta tags and optimize horoscope images for sharing
Implement dynamic meta tag generation for horoscope pages, including daily, weekly, and monthly predictions, and optimize the OG image for improved social sharing previews.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 23852c00-4779-460a-9e0c-d09fee4b6c92
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: dea1fe98-0dfb-4e47-9b94-3c50decbba8a
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/f209e72a-0939-48fa-84fc-57854de71967/23852c00-4779-460a-9e0c-d09fee4b6c92/OPD8Ro3
Replit-Helium-Checkpoint-Created: true
2026-03-08 07:25:10 +00:00

164 lines
7.3 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 { type Express } from "express";
import { createServer as createViteServer, createLogger } from "vite";
import { type Server } from "http";
import viteConfig from "../vite.config";
import fs from "fs";
import path from "path";
import { nanoid } from "nanoid";
import { storage } from "./storage";
const viteLogger = createLogger();
function escapeHtml(str: string): string {
return str.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
}
function ogImageUrl(coverImage: string, baseUrl: string): string {
if (!coverImage) return "";
let imgPath = coverImage;
if (imgPath.endsWith(".webp")) {
imgPath = imgPath.replace(/\.webp$/, ".jpg");
}
return imgPath.startsWith("http") ? imgPath : `${baseUrl}${imgPath}`;
}
function stripExistingMeta(html: string): string {
html = html.replace(/<meta\s+property="og:[^"]*"[^>]*>\s*/gi, "");
html = html.replace(/<meta\s+name="twitter:[^"]*"[^>]*>\s*/gi, "");
html = html.replace(/<meta\s+name="description"[^>]*>\s*/gi, "");
html = html.replace(/<meta\s+name="keywords"[^>]*>\s*/gi, "");
html = html.replace(/<link\s+rel="canonical"[^>]*>\s*/gi, "");
return html;
}
export async function setupVite(server: Server, app: Express) {
const serverOptions = {
middlewareMode: true,
hmr: { server, path: "/vite-hmr" },
allowedHosts: true as const,
};
const vite = await createViteServer({
...viteConfig,
configFile: false,
customLogger: {
...viteLogger,
error: (msg, options) => {
viteLogger.error(msg, options);
process.exit(1);
},
},
server: serverOptions,
appType: "custom",
});
app.use(vite.middlewares);
app.use("/{*path}", async (req, res, next) => {
const url = req.originalUrl;
const canonicalBase = "https://folx.tv";
try {
const clientTemplate = path.resolve(
import.meta.dirname,
"..",
"client",
"index.html",
);
let template = await fs.promises.readFile(clientTemplate, "utf-8");
template = template.replace(
`src="/src/main.tsx"`,
`src="/src/main.tsx?v=${nanoid()}"`,
);
const articleMatch = url.match(/^\/article\/([^?#]+)/);
if (articleMatch) {
try {
const slug = decodeURIComponent(articleMatch[1]);
const article = await storage.getArticleBySlug(slug);
if (article) {
const articleUrl = `${canonicalBase}/article/${article.slug}`;
const imageUrl = ogImageUrl(article.coverImage || "", canonicalBase);
const finalImage = imageUrl || `${canonicalBase}/og-image.jpg`;
template = stripExistingMeta(template);
const ogTags = [
`<meta property="og:title" content="${escapeHtml(article.title)}" />`,
`<meta property="og:description" content="${escapeHtml(article.excerpt)}" />`,
`<meta property="og:type" content="article" />`,
`<meta property="og:url" content="${escapeHtml(articleUrl)}" />`,
`<meta property="og:image" content="${escapeHtml(finalImage)}" />`,
`<meta property="og:image:secure_url" content="${escapeHtml(finalImage)}" />`,
`<meta property="og:image:width" content="800" />`,
`<meta property="og:image:height" content="450" />`,
`<meta property="og:image:type" content="image/jpeg" />`,
`<meta property="og:site_name" content="Folx Music Television" />`,
`<meta property="og:locale" content="de_DE" />`,
`<meta property="fb:pages" content="1428520781492675" />`,
`<meta name="twitter:card" content="summary_large_image" />`,
`<meta name="twitter:title" content="${escapeHtml(article.title)}" />`,
`<meta name="twitter:description" content="${escapeHtml(article.excerpt)}" />`,
`<meta name="twitter:image" content="${escapeHtml(finalImage)}" />`,
`<meta name="description" content="${escapeHtml(article.excerpt)}" />`,
`<meta name="keywords" content="Volksmusik, Schlager, ${escapeHtml(article.title)}, ${escapeHtml(article.category || 'News')}, FOLX TV" />`,
`<link rel="canonical" href="${escapeHtml(articleUrl)}" />`,
`<title>${escapeHtml(article.title)} - Volksmusik & Schlager | Folx Music Television</title>`,
].join("\n ");
template = template.replace(/<title>[^<]*<\/title>/, ogTags);
}
} catch (e) {
}
}
if (url.match(/^\/horoskop(\/|$|\?)/)) {
template = stripExistingMeta(template);
const signMatch = url.match(/^\/horoskop\/([^?#]+)/);
const signName = signMatch ? decodeURIComponent(signMatch[1]) : null;
const capitalSign = signName ? signName.charAt(0).toUpperCase() + signName.slice(1) : null;
const title = capitalSign
? `${capitalSign} Horoskop Tages-, Wochen- & Monatshoroskop | Folx Music Television`
: "Horoskop Tages-, Wochen- & Monatshoroskop für alle Sternzeichen | Folx Music Television";
const desc = capitalSign
? `${capitalSign} Horoskop: Tägliches, wöchentliches und monatliches Horoskop. Liebe, Beruf & Gesundheit bei FOLX TV.`
: "Kostenloses Tageshoroskop, Wochenhoroskop & Monatshoroskop für alle 12 Sternzeichen. Liebe, Beruf, Gesundheit bei FOLX TV.";
const horoUrl = `${canonicalBase}${signName ? `/horoskop/${signName}` : "/horoskop"}`;
const horoImage = `${canonicalBase}/images/horoskop-og.jpg`;
const horoTags = [
`<meta property="og:title" content="${escapeHtml(title)}" />`,
`<meta property="og:description" content="${escapeHtml(desc)}" />`,
`<meta property="og:type" content="website" />`,
`<meta property="og:url" content="${escapeHtml(horoUrl)}" />`,
`<meta property="og:image" content="${escapeHtml(horoImage)}" />`,
`<meta property="og:image:secure_url" content="${escapeHtml(horoImage)}" />`,
`<meta property="og:image:width" content="1200" />`,
`<meta property="og:image:height" content="675" />`,
`<meta property="og:image:type" content="image/jpeg" />`,
`<meta property="og:site_name" content="Folx Music Television" />`,
`<meta property="og:locale" content="de_DE" />`,
`<meta name="twitter:card" content="summary_large_image" />`,
`<meta name="twitter:title" content="${escapeHtml(title)}" />`,
`<meta name="twitter:description" content="${escapeHtml(desc)}" />`,
`<meta name="twitter:image" content="${escapeHtml(horoImage)}" />`,
`<meta name="description" content="${escapeHtml(desc)}" />`,
`<meta name="keywords" content="Horoskop, Tageshoroskop, Wochenhoroskop, Monatshoroskop, Sternzeichen, Volksmusik, FOLX TV${capitalSign ? `, ${capitalSign}` : ""}" />`,
`<link rel="canonical" href="${escapeHtml(horoUrl)}" />`,
`<title>${escapeHtml(title)}</title>`,
].join("\n ");
template = template.replace(/<title>[^<]*<\/title>/, horoTags);
}
const page = await vite.transformIndexHtml(url, template);
res.status(200).set({ "Content-Type": "text/html" }).end(page);
} catch (e) {
vite.ssrFixStacktrace(e as Error);
next(e);
}
});
}