Present FOLX as a music news publisher and unify site SEO

This commit is contained in:
Codex
2026-10-07 13:42:48 +02:00
parent c5c3fe290e
commit cc306921d7
14 changed files with 173 additions and 115 deletions
+42 -44
View File
@@ -1,3 +1,4 @@
import { publisherSchema, homeSchema, INFO_SEO, articleModifiedTime, articleGenre } from "../shared/seo";
import express, { type Express } from "express";
import fs from "fs";
import path from "path";
@@ -11,13 +12,7 @@ import sharp from "sharp";
// Brez markerja ostane vse kot prej (glasbeno), da starejsi clanki ne nazadujejo.
// Besedilne zaznave namenoma NI: preizkus na 30 clankih jo je zgresil pri
// Fernsehgartnu in Carpendaleju.
function folxGenre(article: any): { words: string[]; section: string } {
const m = /<!--\s*folx-badge:\s*([^>]*?)\s*-->/i.exec(String(article?.content || ""));
const badge = (m ? m[1] : "MUSIC NEWS").toUpperCase();
return badge === "MUSIC NEWS"
? { words: ["Volksmusik", "Schlager", "Musik News"], section: "Musik News" }
: { words: ["Star News", "Promi News", "Promi"], section: "Star News" };
}
const folxGenre = (article: any) => articleGenre(String(article?.content || ""));
function escapeHtml(str: string): string {
@@ -88,15 +83,7 @@ const ARTICLE_SLUG_REDIRECTS: Record<string, string> = {
// 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) => ({
// 4. 9. 2026: izdajatelj spet samo "Folx Music Television" z logom, kot do
// 26. 8. - brez registrirane druzbe, naslova in kontaktov.
"@type": "Organization",
"@id": `${base}/#organization`,
"name": "Folx Music Television",
"url": base,
"logo": { "@type": "ImageObject", "url": `${base}/og-image.jpg` },
});
const PUBLISHER = publisherSchema;
const ONE_RUBRIKEN = new Set(["Politik", "Promis", "Sport", "Musik & Film", "Wetter", "Finanzen", "Aktuelles"]);
@@ -294,28 +281,10 @@ export function serveStatic(app: Express) {
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",
},
},
],
};
const homeLd = homeSchema(canonicalBase);
template = template.replace(
"</head>",
`<script type="application/ld+json">${JSON.stringify(homeLd).replace(/</g, "\\u003c")}</script>\n</head>`);
`<script id="home-jsonld" type="application/ld+json">${JSON.stringify(homeLd).replace(/</g, "\\u003c")}</script>\n</head>`);
// 31. 8. 2026: naslovnica je React aplikacija, zato je robot dobil samo
// lupino brez ene same povezave na clanek. Clanki se do Googla pridejo
@@ -325,7 +294,8 @@ export function serveStatic(app: Express) {
try {
const vsi = await storage.getArticles();
const zadnji = [...vsi]
.sort((a: any, b: any) => String(b.publishedAt).localeCompare(String(a.publishedAt)))
.filter((a: any) => !a.targetUrl && !ONE_RUBRIKEN.has(a.category || ""))
.sort((a: any, b: any) => new Date(b.publishedAt).getTime() - new Date(a.publishedAt).getTime())
.slice(0, 40);
if (zadnji.length) {
const seznam = zadnji
@@ -503,11 +473,7 @@ export function serveStatic(app: Express) {
: 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;
})();
const modDate = articleModifiedTime(article.content || "", pubDate);
// VideoObject: ce je v clanku vgrajen YouTube video
const videoId = (() => {
@@ -518,6 +484,8 @@ export function serveStatic(app: Express) {
const jsonLd = {
"@context": "https://schema.org",
"@type": "NewsArticle",
"@id": `${articleUrl}#article`,
"inLanguage": "de-DE",
"headline": article.title,
"description": article.excerpt,
"image": [finalImage],
@@ -536,7 +504,7 @@ export function serveStatic(app: Express) {
"@id": articleUrl,
},
};
const jsonLdTag = `<script type="application/ld+json">${JSON.stringify(jsonLd).replace(/</g, "\\u003c")}</script>`;
const jsonLdTag = `<script id="article-jsonld" type="application/ld+json">${JSON.stringify(jsonLd).replace(/</g, "\\u003c")}</script>`;
const videoLd = videoId ? {
"@context": "https://schema.org",
@@ -557,6 +525,8 @@ export function serveStatic(app: Express) {
`<meta property="og:title" content="${escapeHtml(article.title)}" />`,
`<meta property="og:description" content="${escapeHtml(article.excerpt)}" />`,
`<meta property="og:type" content="article" />`,
`<meta property="article:published_time" content="${pubDate}" />`,
`<meta property="article:modified_time" content="${modDate}" />`,
`<meta property="og:url" content="${escapeHtml(articleUrl)}" />`,
`<meta property="og:image" content="${escapeHtml(finalImage)}" />`,
`<meta property="og:image:secure_url" content="${escapeHtml(finalImage)}" />`,
@@ -575,7 +545,7 @@ export function serveStatic(app: Express) {
`<meta name="description" content="${escapeHtml(article.excerpt)}" />`,
`<meta name="keywords" content="${escapeHtml(folxGenre(article).words.join(", "))}, ${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>`,
`<title>${escapeHtml(article.title)} | FOLX TV</title>`,
jsonLdTag,
videoLdTag,
].join("\n ");
@@ -765,6 +735,34 @@ export function serveStatic(app: Express) {
return;
}
const infoPath = pathOnly.replace(/\/+$/, "");
const info = INFO_SEO[infoPath];
if (info) {
let template = stripExistingMeta(await fs.promises.readFile(indexPath, "utf-8"));
const pageUrl = `${canonicalBase}${infoPath}`;
const title = `${info.title} | FOLX TV`;
const ld = { "@context": "https://schema.org", "@type": info.type,
"@id": `${pageUrl}#webpage`, url: pageUrl, name: title, description: info.description,
inLanguage: "de-DE", isPartOf: { "@id": `${canonicalBase}/#website` }, publisher: PUBLISHER(canonicalBase) };
template = template.replace(/<title>[^<]*<\/title>/, [
`<title>${escapeHtml(title)}</title>`,
`<meta name="description" content="${escapeHtml(info.description)}" />`,
`<link rel="canonical" href="${pageUrl}" />`,
`<meta property="og:title" content="${escapeHtml(title)}" />`,
`<meta property="og:description" content="${escapeHtml(info.description)}" />`,
`<meta property="og:type" content="website" />`,
`<meta property="og:url" content="${pageUrl}" />`,
`<meta property="og:image" content="${canonicalBase}/og-image.jpg" />`,
`<meta name="twitter:card" content="summary_large_image" />`,
`<meta name="twitter:title" content="${escapeHtml(title)}" />`,
`<meta name="twitter:description" content="${escapeHtml(info.description)}" />`,
`<meta name="twitter:image" content="${canonicalBase}/og-image.jpg" />`,
`<script id="info-jsonld" type="application/ld+json">${JSON.stringify(ld).replace(/</g, "\\u003c")}</script>`,
].join("\n"));
res.status(200).type("html").send(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 = [