329 lines
16 KiB
TypeScript
329 lines
16 KiB
TypeScript
import express, { type Express } from "express";
|
||
import fs from "fs";
|
||
import path from "path";
|
||
import { storage } from "./storage";
|
||
|
||
function escapeHtml(str: string): string {
|
||
return str.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
||
}
|
||
|
||
function ogImageUrl(coverImage: string, baseUrl: string): string {
|
||
if (!coverImage) return "";
|
||
let imgPath = coverImage;
|
||
// Cloudinary: vsili FB razmerje 1200x630 z g_faces (da FB ne reze glav)
|
||
if (imgPath.includes("res.cloudinary.com")) {
|
||
imgPath = imgPath.replace(/([,&/?])h_\d+/i, "$1h_630").replace(/([,&/?])w_\d+/i, "$1w_1200");
|
||
// ce ni g_faces, ga dodaj
|
||
if (!/g_faces/.test(imgPath)) {
|
||
imgPath = imgPath.replace(/\/(image\/(upload|fetch))\//, "/$1/g_faces,");
|
||
}
|
||
return imgPath;
|
||
}
|
||
if (imgPath.endsWith(".webp")) {
|
||
imgPath = imgPath.replace(/\.webp$/, ".jpg");
|
||
}
|
||
return imgPath.startsWith("http") ? imgPath : `${baseUrl}${imgPath}`;
|
||
}
|
||
|
||
// Rubrike, ki jih pisemo enkrat in objavimo tudi na onemusic.news. Besedilo je
|
||
// isto na obeh domenah, zato jih na folx.tv drzimo zunaj Googlovega indeksa —
|
||
// v bazi in v /api/articles ostanejo, ker jih bere sidebar na TV kanalih.
|
||
const ONE_RUBRIKEN = new Set(["Politik", "Promis", "Sport", "Musik & Film", "Wetter", "Finanzen", "Aktuelles"]);
|
||
|
||
function setRobots(html: string, content: string): string {
|
||
const tag = `<meta name="robots" content="${content}" />`;
|
||
if (/<meta\s+name="robots"[^>]*>/i.test(html)) {
|
||
return html.replace(/<meta\s+name="robots"[^>]*>/i, tag);
|
||
}
|
||
return html.replace("</head>", ` ${tag}\n </head>`);
|
||
}
|
||
|
||
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 function serveStatic(app: Express) {
|
||
const distPath = path.resolve(__dirname, "public");
|
||
if (!fs.existsSync(distPath)) {
|
||
throw new Error(
|
||
`Could not find the build directory: ${distPath}, make sure to build the client first`,
|
||
);
|
||
}
|
||
|
||
app.use(express.static(distPath, {
|
||
setHeaders(res, filePath) {
|
||
if (filePath.endsWith("og-image.jpg") || filePath.includes("/uploads/") || filePath.includes("/images/")) {
|
||
res.setHeader("Cache-Control", "public, max-age=86400");
|
||
}
|
||
},
|
||
}));
|
||
|
||
app.use("/{*path}", async (req, res) => {
|
||
const url = req.originalUrl;
|
||
const indexPath = path.resolve(distPath, "index.html");
|
||
|
||
const canonicalBase = "https://folx.tv";
|
||
const host = req.get("host") || "folx.tv";
|
||
const protocol = req.get("x-forwarded-proto") || "https";
|
||
const baseUrl = `${protocol}://${host}`;
|
||
|
||
const articleMatch = url.match(/^\/article\/([^?#]+)/);
|
||
if (articleMatch) {
|
||
try {
|
||
const slug = decodeURIComponent(articleMatch[1]);
|
||
const article = await storage.getArticleBySlug(slug);
|
||
if (article && (article as any).targetUrl) {
|
||
res.redirect(302, (article as any).targetUrl);
|
||
return;
|
||
}
|
||
if (article) {
|
||
const articleUrl = `${canonicalBase}/article/${article.slug}`;
|
||
const imageUrl = ogImageUrl(article.coverImage || "", canonicalBase);
|
||
const finalImage = imageUrl || `${canonicalBase}/og-image.jpg`;
|
||
|
||
let template = await fs.promises.readFile(indexPath, "utf-8");
|
||
template = stripExistingMeta(template);
|
||
|
||
const pubDate = article.publishedAt
|
||
? new Date(article.publishedAt as any).toISOString()
|
||
: new Date().toISOString();
|
||
|
||
// dateModified: ce je clanek dopolnjen (enrich), preberi marker <!--folx-updated:ISO-->
|
||
const modDate = (() => {
|
||
const mm = /<!--folx-updated:([^>]+?)-->/.exec(article.content || "");
|
||
if (mm) { try { return new Date(mm[1]).toISOString(); } catch (e) { /* ignore */ } }
|
||
return pubDate;
|
||
})();
|
||
|
||
// VideoObject: ce je v clanku vgrajen YouTube video
|
||
const videoId = (() => {
|
||
const mm = /youtube(?:-nocookie)?\.com\/embed\/([A-Za-z0-9_-]{6,})/.exec(article.content || "");
|
||
return mm ? mm[1] : null;
|
||
})();
|
||
|
||
const jsonLd = {
|
||
"@context": "https://schema.org",
|
||
"@type": "NewsArticle",
|
||
"headline": article.title,
|
||
"description": article.excerpt,
|
||
"image": [finalImage],
|
||
"datePublished": pubDate,
|
||
"dateModified": modDate,
|
||
"articleSection": article.category || "News",
|
||
"author": {
|
||
"@type": "Organization",
|
||
"name": article.author || "Folx Music Television",
|
||
"url": canonicalBase,
|
||
},
|
||
"publisher": {
|
||
"@type": "Organization",
|
||
"name": "Folx Music Television",
|
||
"logo": {
|
||
"@type": "ImageObject",
|
||
"url": `${canonicalBase}/og-image.jpg`,
|
||
},
|
||
},
|
||
"mainEntityOfPage": {
|
||
"@type": "WebPage",
|
||
"@id": articleUrl,
|
||
},
|
||
};
|
||
const jsonLdTag = `<script type="application/ld+json">${JSON.stringify(jsonLd).replace(/</g, "\\u003c")}</script>`;
|
||
|
||
const videoLd = videoId ? {
|
||
"@context": "https://schema.org",
|
||
"@type": "VideoObject",
|
||
"name": article.title,
|
||
"description": article.excerpt,
|
||
"thumbnailUrl": [`https://i.ytimg.com/vi/${videoId}/maxresdefault.jpg`],
|
||
"uploadDate": pubDate,
|
||
"embedUrl": `https://www.youtube.com/embed/${videoId}`,
|
||
"contentUrl": `https://www.youtube.com/watch?v=${videoId}`,
|
||
"publisher": {
|
||
"@type": "Organization",
|
||
"name": "Folx Music Television",
|
||
"logo": { "@type": "ImageObject", "url": `${canonicalBase}/og-image.jpg` },
|
||
},
|
||
} : null;
|
||
const videoLdTag = videoLd
|
||
? `<script type="application/ld+json">${JSON.stringify(videoLd).replace(/</g, "\\u003c")}</script>`
|
||
: "";
|
||
|
||
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="1200" />`,
|
||
`<meta property="og:image:height" content="630" />`,
|
||
`<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="281200958640258" />`,
|
||
`<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>`,
|
||
jsonLdTag,
|
||
videoLdTag,
|
||
].join("\n ");
|
||
|
||
template = template.replace(/<title>[^<]*<\/title>/, ogTags);
|
||
|
||
// SSR: vgradi vsebino clanka v HTML, da jo Google/Discover vidita brez JS
|
||
const ssrBlock = [
|
||
'<article class="ssr-content">',
|
||
`<h1>${escapeHtml(article.title)}</h1>`,
|
||
imageUrl ? `<img src="${escapeHtml(imageUrl)}" alt="${escapeHtml(article.title)}" style="max-width:100%;height:auto;" />` : "",
|
||
`<p><strong>${escapeHtml(article.excerpt)}</strong></p>`,
|
||
article.content || "",
|
||
"</article>",
|
||
].join("\n");
|
||
template = template.replace('<div id="root"></div>', () => `<div id="root">${ssrBlock}</div>`);
|
||
|
||
if (ONE_RUBRIKEN.has(article.category || "")) {
|
||
template = setRobots(template, "noindex, follow, max-image-preview:large");
|
||
}
|
||
|
||
res.status(200).set({ "Content-Type": "text/html" }).end(template);
|
||
return;
|
||
}
|
||
|
||
// Clanka s tem slugom ni. Brez tega bi SPA vrnil HTTP 200 z naslovnico
|
||
// in canonical na "/" — Google to bere kot podvojeno naslovnico (soft 404),
|
||
// izbrisani clanki pa ostanejo v indeksu in v Discovru.
|
||
{
|
||
let template = await fs.promises.readFile(indexPath, "utf-8");
|
||
template = stripExistingMeta(template);
|
||
template = template.replace(
|
||
/<title>[^<]*<\/title>/,
|
||
[
|
||
'<meta name="robots" content="noindex, follow" />',
|
||
'<meta name="description" content="Dieser Artikel ist nicht mehr verfuegbar." />',
|
||
'<title>Artikel nicht gefunden | Folx Music Television</title>',
|
||
].join("\n "),
|
||
);
|
||
res.status(404).set({ "Content-Type": "text/html" }).end(template);
|
||
return;
|
||
}
|
||
} catch (e) {
|
||
}
|
||
}
|
||
|
||
if (url.match(/^\/gallery(\/|$|\?)/)) {
|
||
let template = await fs.promises.readFile(indexPath, "utf-8");
|
||
template = stripExistingMeta(template);
|
||
const galTitle = "Fotogalerie \u2013 Einzigartige Momente von unseren Aufzeichnungen | FOLX TV";
|
||
const galDesc = "Spannende Schnappsch\u00fcsse und besondere Augenblicke, eingefangen bei unseren Aufzeichnungen. Authentische Bilder der Volksmusik- und Schlager-Stars in einzigartigen Momenten bei FOLX TV.";
|
||
const galUrl = `${canonicalBase}/gallery`;
|
||
const galImage = `${canonicalBase}/images/og-image.jpg`;
|
||
|
||
const galTags = [
|
||
`<meta property="og:title" content="${escapeHtml(galTitle)}" />`,
|
||
`<meta property="og:description" content="${escapeHtml(galDesc)}" />`,
|
||
`<meta property="og:type" content="website" />`,
|
||
`<meta property="og:url" content="${escapeHtml(galUrl)}" />`,
|
||
`<meta property="og:image" content="${escapeHtml(galImage)}" />`,
|
||
`<meta property="og:image:secure_url" content="${escapeHtml(galImage)}" />`,
|
||
`<meta property="og:image:width" content="1200" />`,
|
||
`<meta property="og:image:height" content="630" />`,
|
||
`<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(galTitle)}" />`,
|
||
`<meta name="twitter:description" content="${escapeHtml(galDesc)}" />`,
|
||
`<meta name="twitter:image" content="${escapeHtml(galImage)}" />`,
|
||
`<meta name="description" content="${escapeHtml(galDesc)}" />`,
|
||
`<meta name="keywords" content="Fotogalerie, Volksmusik Fotos, Schlager Bilder, Aufzeichnungen, Schnappschüsse, FOLX TV, Volksmusik Stars" />`,
|
||
`<link rel="canonical" href="${escapeHtml(galUrl)}" />`,
|
||
`<title>${escapeHtml(galTitle)}</title>`,
|
||
].join("\n ");
|
||
|
||
template = template.replace(/<title>[^<]*<\/title>/, galTags);
|
||
res.status(200).set({ "Content-Type": "text/html" }).end(template);
|
||
return;
|
||
}
|
||
|
||
if (url.match(/^\/horoskop(\/|$|\?)/)) {
|
||
let template = await fs.promises.readFile(indexPath, "utf-8");
|
||
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);
|
||
res.status(200).set({ "Content-Type": "text/html" }).end(template);
|
||
return;
|
||
}
|
||
|
||
// Neznana pot mora vrniti PRAVI 404, sicer Google dobi soft 404 (HTTP 200
|
||
// z naslovnico). Seznam mora ustrezati potem v client/src/App.tsx.
|
||
const KNOWN = [
|
||
/^\/$/, /^\/search\/?$/, /^\/article\//, /^\/category\//, /^\/aktuelles\/?$/,
|
||
/^\/videos\/?$/, /^\/gallery\/?$/, /^\/horoskop(\/|$)/, /^\/rezepte\/?$/,
|
||
/^\/empfang-folx-tv\/?$/, /^\/ueber-uns\/?$/, /^\/impressum\/?$/,
|
||
/^\/datenschutz\/?$/, /^\/kontakt\/?$/, /^\/admin(\/|$)/,
|
||
];
|
||
const cleanPath = url.split("?")[0].split("#")[0];
|
||
const known = KNOWN.some((re) => re.test(cleanPath));
|
||
|
||
let template = await fs.promises.readFile(indexPath, "utf-8");
|
||
if (!known) {
|
||
template = stripExistingMeta(template);
|
||
template = template.replace(
|
||
/<title>[^<]*<\/title>/,
|
||
[
|
||
'<title>Seite nicht gefunden | FOLX TV</title>',
|
||
'<meta name="robots" content="noindex, follow" />',
|
||
].join("\n ")
|
||
);
|
||
res.status(404).set({ "Content-Type": "text/html" }).end(template);
|
||
return;
|
||
}
|
||
res.status(200).set({ "Content-Type": "text/html" }).end(template);
|
||
});
|
||
}
|