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, "&").replace(/"/g, """).replace(//g, ">"); } // 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(); 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 = { "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 = ``; if (/]*>/i.test(html)) { return html.replace(/]*>/i, tag); } return html.replace("", ` ${tag}\n `); } function stripExistingMeta(html: string): string { html = html.replace(/]*>\s*/gi, ""); html = html.replace(/]*>\s*/gi, ""); html = html.replace(/]*>\s*/gi, ""); html = html.replace(/]*>\s*/gi, ""); html = html.replace(/]*>\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/: 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>/, [ '<meta name="robots" content="noindex, follow" />', '<title>Video nicht gefunden | Folx Music Television', ].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=. 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 = [ ``, ``, ``, ``, ``, ``, ``, ``, ``, ``, ``, ``, ``, ``, ``, `${escapeHtml(v.shortTitle)} | ${escapeHtml(v.rubrik)} | Folx Music Television`, ``, ].join("\n "); template = template.replace(/[^<]*<\/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> – ${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`, ].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 = [ ``, ``, ``, ``, ``, ``, ``, ``, ``, ``, ``, ``, `${escapeHtml(catTitle)}`, ``, ].join("\n "); template = template.replace(/[^<]*<\/title>/, tags); // Rubrike, ki sestavljajo /aktuelles, niso del indeksiranega dela strani: // clanki so ze noindex, zato tudi njihov seznam ne sme v indeks. if (Array.from(ONE_RUBRIKEN).some((r) => r.toLowerCase() === cat.toLowerCase())) { template = setRobots(template, "noindex, follow"); } 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`, jsonLdTag, videoLdTag, ].join("\n "); template = template.replace(/[^<]*<\/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', ].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 = [ ``, ``, ``, ``, ``, ``, ``, ``, ``, ``, ``, ``, ``, ``, ``, ``, ``, ``, `${escapeHtml(galTitle)}`, ].join("\n "); template = template.replace(/[^<]*<\/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)}`, ].join("\n "); template = template.replace(/[^<]*<\/title>/, horoTags); res.status(200).set({ "Content-Type": "text/html" }).end(template); return; } // ---- /aktuelles: zbirna stran rubrik, ki so vse noindex. Prej je vracala // HTML naslovnice s canonical "/", kar je Google bral kot podvojeno // naslovnico. Zdaj ima svoj canonical in noindex, follow. ---- if (/^\/aktuelles\/?$/.test(url.split("?")[0].split("#")[0])) { let template = await fs.promises.readFile(indexPath, "utf-8"); template = stripExistingMeta(template); const aktTitle = "Aktuelles | Folx Music Television"; template = template.replace(/<title>[^<]*<\/title>/, [ `<link rel="canonical" href="${canonicalBase}/aktuelles" />`, `<title>${escapeHtml(aktTitle)}`, ].join("\n ")); template = setRobots(template, "noindex, follow"); 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>Seite nicht gefunden | FOLX TV', '', ].join("\n ") ); res.status(404).set({ "Content-Type": "text/html" }).end(template); return; } res.status(200).set({ "Content-Type": "text/html" }).end(template); }); }