Files
folx-tv/server/static.ts
T

636 lines
30 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 Express } from "express";
import fs from "fs";
import path from "path";
import { storage } from "./storage";
import sharp from "sharp";
function escapeHtml(str: string): string {
return str.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
}
// Dejanske dimenzije naslovnice. Prej sta bili sirina in visina v meta
// oznakah fiksno zapisani kot 1200x630, slike pa so v resnici 1200x900 —
// Facebook in Google sta dobila napacno vrednost. Ce dimenzij ne moremo
// prebrati, oznak raje ne zapisemo.
const ogDimCache = new Map<string, { w: number; h: number } | null>();
async function ogImageDims(imgUrl: string, publicDir: string) {
if (ogDimCache.has(imgUrl)) return ogDimCache.get(imgUrl) ?? null;
let dims: { w: number; h: number } | null = null;
try {
const m = /\/(uploads|images)\/([^/?#]+)$/.exec(imgUrl);
if (m) {
const cand = [
path.join(publicDir, m[1], m[2]),
path.join(publicDir, m[1], m[2].replace(/\.jpg$/, ".webp")),
];
for (const p of cand) {
if (!fs.existsSync(p)) continue;
const meta = await sharp(p).metadata();
if (meta.width && meta.height) { dims = { w: meta.width, h: meta.height }; break; }
}
}
} catch (e) {
dims = null;
}
ogDimCache.set(imgUrl, dims);
return dims;
}
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}`;
}
// Stari slugi clankov, ki so bili pozneje preimenovani. Brez tega vsak star
// link (FB objave, komentarji, go.folx.tv kratki linki, Googlov indeks) pade
// na trdi 404. Kljuc = star slug, vrednost = trenutni slug.
const ARTICLE_SLUG_REDIRECTS: Record<string, string> = {
"alexander-rier-verraet-das-ist-die-neue-frau-an-seiner-seite":
"alexander-rier-von-mann-zu-mann-immer-wieder-sonntags",
// 25. 8. 2026 izbrisan dvojnik: bot je isto zgodbo objavil dvakrat.
// Andreas Martin je bil vrnjen (bil je ze v Search Console), zato tu ni.
"isi-glueck-sagt-konzerte-ab-stimme-in-gefahr":
"isi-glueck-sagt-konzerte-ab-sorge-um-ihre-stimme",
};
// 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.
// Izdajatelj: doslej samo ime in logo. Brez registrirane druzbe, naslova in
// profilov Google znamke nima s cim povezati.
const PUBLISHER = (base: string) => ({
"@type": ["NewsMediaOrganization", "Organization"],
"@id": `${base}/#organization`,
"name": "FOLX Music Television",
"url": base,
"legalName": "BoldFrame Productions d.o.o.",
"vatID": "SI38853507",
"logo": { "@type": "ImageObject", "url": `${base}/og-image.jpg` },
"address": {
"@type": "PostalAddress",
"streetAddress": "Sokolska ulica 46",
"postalCode": "2000",
"addressLocality": "Maribor",
"addressCountry": "SI",
},
"contactPoint": {
"@type": "ContactPoint",
"contactType": "customer service",
"email": "office@boldframe.productions",
"areaServed": ["DE", "AT", "CH"],
"availableLanguage": ["German"],
},
"sameAs": [
"https://www.facebook.com/folxtv",
"https://www.instagram.com/folxtv/",
"https://www.youtube.com/@FOLXTV",
],
});
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, {
// brez tega express.static sam postrezhe index.html za "/" in nasa
// naslovnica nikoli ne pride do handlerja, ki ji doda strukturirane podatke
index: false,
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}`;
// ---- /video/<slug>: napovednik je predvajljiv na strani, cel video na YouTube ----
const ytVideoMatch = url.match(/^\/video\/([^?#\/]+)/);
if (ytVideoMatch) {
try {
const slug = decodeURIComponent(ytVideoMatch[1]);
const idxFile = path.resolve(process.cwd(), "client/public/uploads/video/index.json");
const list: any[] = JSON.parse(await fs.promises.readFile(idxFile, "utf-8")).items || [];
const v = list.find((i) => i.slug === slug || i.ytId === slug);
let template = await fs.promises.readFile(indexPath, "utf-8");
template = stripExistingMeta(template);
if (!v) {
template = template.replace(/<title>[^<]*<\/title>/, [
'<meta name="robots" content="noindex, follow" />',
'<title>Video nicht gefunden | Folx Music Television</title>',
].join("\n "));
res.status(404).set({ "Content-Type": "text/html" }).end(template);
return;
}
const pageUrl = `${canonicalBase}/video/${v.slug}`;
const poster = `${canonicalBase}${v.posterUrl}`;
const desc = (v.teaser || v.shortTitle || v.title).slice(0, 300);
const longDesc = String(v.descriptionClean || v.description || v.teaser || v.shortTitle)
.replace(/https?:\/\/\S+/g, "").replace(/\s+/g, " ").trim().slice(0, 900);
const mins = Math.floor((v.durationS || 0) / 60), secs = (v.durationS || 0) % 60;
const embedUrl = v.embedUrl || `https://www.youtube-nocookie.com/embed/${v.ytId}`;
// Poglavja: iz "1:24" naredimo sekunde, da jih lahko damo v Clip
// oznacbo (Google iz nje naredi "Wichtige Momente" pod videom) in da
// je vsako poglavje klikljiva povezava z ?t=<sekunde>.
const chapSecs = (t: string) => {
const p = String(t).split(":").map((x) => parseInt(x, 10) || 0);
return p.length === 3 ? p[0] * 3600 + p[1] * 60 + p[2] : p[0] * 60 + p[1];
};
const chaps = (v.chapters || [])
.map((c: any) => ({ ...c, s: chapSecs(c.t) }))
.filter((c: any) => c.s < (v.durationS || 0))
.sort((a: any, b: any) => a.s - b.s);
const startAt = parseInt(String((req.query as any)?.t || ""), 10);
const embedSrc = Number.isFinite(startAt) && startAt > 0
? `${embedUrl}?start=${startAt}` : embedUrl;
const videoLd = {
"@context": "https://schema.org",
"@type": "VideoObject",
"name": v.shortTitle,
"description": longDesc || desc,
"thumbnailUrl": [poster],
"uploadDate": v.publishedAt,
"duration": `PT${mins}M${secs}S`,
"embedUrl": embedUrl,
"contentUrl": v.youtubeUrl,
"isFamilyFriendly": true,
"inLanguage": "de",
"publisher": PUBLISHER(canonicalBase),
"mainEntityOfPage": { "@type": "WebPage", "@id": pageUrl },
...(chaps.length >= 3 ? {
hasPart: chaps.map((c: any, i: number) => ({
"@type": "Clip",
"name": c.label,
"startOffset": c.s,
"endOffset": i + 1 < chaps.length ? chaps[i + 1].s : (v.durationS || c.s + 30),
"url": `${pageUrl}?t=${c.s}`,
})),
potentialAction: {
"@type": "SeekToAction",
"target": `${pageUrl}?t={seek_to_second_number}`,
"startOffset-input": "required name=seek_to_second_number",
},
} : {}),
};
const tags = [
`<meta property="og:title" content="${escapeHtml(v.shortTitle)}" />`,
`<meta property="og:description" content="${escapeHtml(desc)}" />`,
`<meta property="og:type" content="video.other" />`,
`<meta property="og:url" content="${escapeHtml(pageUrl)}" />`,
`<meta property="og:image" content="${escapeHtml(poster)}" />`,
`<meta property="og:image:width" content="1280" />`,
`<meta property="og:image:height" content="720" />`,
`<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(v.shortTitle)}" />`,
`<meta name="twitter:description" content="${escapeHtml(desc)}" />`,
`<meta name="twitter:image" content="${escapeHtml(poster)}" />`,
`<meta name="description" content="${escapeHtml(desc)}" />`,
`<link rel="canonical" href="${escapeHtml(pageUrl)}" />`,
`<title>${escapeHtml(v.shortTitle)} | ${escapeHtml(v.rubrik)} | Folx Music Television</title>`,
`<script type="application/ld+json">${JSON.stringify(videoLd).replace(/</g, "\\u003c")}</script>`,
].join("\n ");
template = template.replace(/<title>[^<]*<\/title>/, tags);
const chapterHtml = chaps.length
? `<h2>Kapitel</h2><ul>${chaps.map((c: any) => `<li><a href="${escapeHtml(pageUrl)}?t=${c.s}">${escapeHtml(c.t)}</a> &ndash; ${escapeHtml(c.label)}</li>`).join("")}</ul>`
: "";
// Celoten opis gre v SSR, da ga Google prebere brez JavaScripta.
// Prej je bil v HTML samo naslov in teaser (~320 znakov), ostalo je
// izrisal sele React.
const descHtml = String(v.descriptionClean || v.description || "")
.split(/\n{2,}/)
.map((par: string) => par.trim())
.filter((par: string) => par.length > 2 && !/^https?:\/\//.test(par) && !/^[\u2013\u2014-]{4,}$/.test(par))
.map((par: string) => `<p>${escapeHtml(par).replace(/\n/g, "<br />")}</p>`)
.join("");
const ssr = [
'<article class="ssr-content">',
`<h1>${escapeHtml(v.shortTitle)}</h1>`,
`<iframe width="640" height="360" src="${escapeHtml(embedSrc)}" title="${escapeHtml(v.shortTitle)}" allow="accelerometer; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen style="border:0;max-width:100%;"></iframe>`,
`<p><strong>${escapeHtml(desc)}</strong></p>`,
chapterHtml,
`<p><a href="${escapeHtml(v.youtubeUrl)}" target="_blank" rel="noopener">Das ganze Video auf YouTube ansehen</a></p>`,
v.articleUrl ? `<p><a href="${escapeHtml(v.articleUrl)}">Ganzer Artikel auf folx.tv</a></p>` : "",
descHtml ? `<h2>Zum Video</h2>${descHtml}` : "",
"</article>",
].join("\n");
template = template.replace('<div id="root"></div>', () => `<div id="root">${ssr}</div>`);
res.status(200).set({ "Content-Type": "text/html" }).end(template);
return;
} catch (e) {
}
}
// ---- Naslovnica: doslej brez strukturiranih podatkov. Organization in
// WebSite sta tista dva vozlisca, na katera se veze vse ostalo. ----
if (url === "/" || url.startsWith("/?")) {
try {
let template = await fs.promises.readFile(indexPath, "utf-8");
const homeLd = {
"@context": "https://schema.org",
"@graph": [
PUBLISHER(canonicalBase),
{
"@type": "WebSite",
"@id": `${canonicalBase}/#website`,
"url": `${canonicalBase}/`,
"name": "FOLX Music Television",
"inLanguage": "de-DE",
"publisher": { "@id": `${canonicalBase}/#organization` },
"potentialAction": {
"@type": "SearchAction",
"target": { "@type": "EntryPoint", "urlTemplate": `${canonicalBase}/suche?q={search_term_string}` },
"query-input": "required name=search_term_string",
},
},
],
};
template = template.replace(
"</head>",
`<script type="application/ld+json">${JSON.stringify(homeLd).replace(/</g, "\\u003c")}</script>\n</head>`);
res.status(200).set({ "Content-Type": "text/html" }).end(template);
return;
} catch (e) {
}
}
// ---- /category/<ime>: prej je vrnil HTML naslovnice s canonical "/" in
// brez vsebine, kar je Google bral kot soft 404 / podvojeno naslovnico ----
const catMatch = url.match(/^\/category\/([^?#\/]+)/);
if (catMatch) {
try {
const cat = decodeURIComponent(catMatch[1]);
const all = await storage.getArticles();
const list = all
.filter((a: any) => (a.category || "").toLowerCase() === cat.toLowerCase() && !a.targetUrl)
.slice(0, 40);
let template = await fs.promises.readFile(indexPath, "utf-8");
template = stripExistingMeta(template);
if (!list.length) {
template = template.replace(/<title>[^<]*<\/title>/, [
'<meta name="robots" content="noindex, follow" />',
`<title>${escapeHtml(cat)} | Folx Music Television</title>`,
].join("\n "));
res.status(404).set({ "Content-Type": "text/html" }).end(template);
return;
}
const catUrl = `${canonicalBase}/category/${encodeURIComponent(cat)}`;
const catTitle = `${cat} – Volksmusik & Schlager | Folx Music Television`;
const catDesc = `Alle Beiträge aus der Rubrik ${cat} bei FOLX TV: aktuelle Meldungen, Interviews und Videos aus Volksmusik und Schlager.`;
const firstImg = ogImageUrl(list[0]?.coverImage || "", canonicalBase) || `${canonicalBase}/og-image.jpg`;
const itemLd = {
"@context": "https://schema.org",
"@type": "CollectionPage",
"name": catTitle,
"description": catDesc,
"url": catUrl,
"mainEntity": {
"@type": "ItemList",
"itemListElement": list.slice(0, 20).map((a: any, i: number) => ({
"@type": "ListItem",
"position": i + 1,
"url": `${canonicalBase}/article/${a.slug}`,
"name": a.title,
})),
},
};
const tags = [
`<meta property="og:title" content="${escapeHtml(catTitle)}" />`,
`<meta property="og:description" content="${escapeHtml(catDesc)}" />`,
`<meta property="og:type" content="website" />`,
`<meta property="og:url" content="${escapeHtml(catUrl)}" />`,
`<meta property="og:image" content="${escapeHtml(firstImg)}" />`,
`<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(catTitle)}" />`,
`<meta name="twitter:image" content="${escapeHtml(firstImg)}" />`,
`<meta name="description" content="${escapeHtml(catDesc)}" />`,
`<link rel="canonical" href="${escapeHtml(catUrl)}" />`,
`<title>${escapeHtml(catTitle)}</title>`,
`<script type="application/ld+json">${JSON.stringify(itemLd).replace(/</g, "\\u003c")}</script>`,
].join("\n ");
template = template.replace(/<title>[^<]*<\/title>/, tags);
const ssr = [
'<section class="ssr-content">',
`<h1>${escapeHtml(cat)}</h1>`,
"<ul>",
...list.map((a: any) => {
const img = ogImageUrl(a.coverImage || "", canonicalBase);
return `<li><a href="/article/${escapeHtml(a.slug)}">` +
(img ? `<img src="${escapeHtml(img)}" alt="${escapeHtml(a.title)}" width="320" style="max-width:100%;height:auto;" /> ` : "") +
`<strong>${escapeHtml(a.title)}</strong></a><p>${escapeHtml(a.excerpt || "")}</p></li>`;
}),
"</ul>",
"</section>",
].join("\n");
template = template.replace('<div id="root"></div>', () => `<div id="root">${ssr}</div>`);
res.status(200).set({ "Content-Type": "text/html" }).end(template);
return;
} catch (e) {
}
}
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`;
const articleImgDims = await ogImageDims(finalImage, distPath);
if (!imageUrl) {
console.warn(`[og] clanek brez naslovnice, pada na logotip: ${article.slug}`);
}
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": PUBLISHER(canonicalBase),
"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": PUBLISHER(canonicalBase),
} : 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)}" />`,
...(articleImgDims
? [`<meta property="og:image:width" content="${articleImgDims.w}" />`,
`<meta property="og:image:height" content="${articleImgDims.h}" />`]
: []),
`<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;
}
// Star slug preimenovanega clanka -> trajna preusmeritev na novega.
const renamedTo = ARTICLE_SLUG_REDIRECTS[slug];
if (renamedTo) {
res.redirect(301, `/article/${renamedTo}`);
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\/?$/, /^\/video\/[^/]+\/?$/, /^\/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);
});
}