video sekcija: strani /video/<slug> z 20s napovednikom (predvajljiv na strani, VideoObject) in gumbom na YouTube; API /api/yt-videos; ingest skripta za FOLX YouTube kanal

This commit is contained in:
folx bot
2026-08-23 10:08:17 +02:00
parent 3c9da492a2
commit c8b7def69c
7 changed files with 1812 additions and 1 deletions
+81 -1
View File
@@ -80,6 +80,86 @@ export function serveStatic(app: Express) {
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 mins = Math.floor(v.previewS / 60), secs = v.previewS % 60;
const videoLd = {
"@context": "https://schema.org",
"@type": "VideoObject",
"name": `${v.shortTitle} \u2013 Vorschau`,
"description": desc,
"thumbnailUrl": [poster],
"uploadDate": v.publishedAt,
"duration": `PT${mins}M${secs}S`,
"contentUrl": `${canonicalBase}${v.previewUrl}`,
"isFamilyFriendly": true,
"inLanguage": "de",
"publisher": {
"@type": "Organization",
"name": "Folx Music Television",
"logo": { "@type": "ImageObject", "url": `${canonicalBase}/og-image.jpg` },
},
"mainEntityOfPage": { "@type": "WebPage", "@id": pageUrl },
};
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 = (v.chapters || []).length
? `<h2>Kapitel</h2><ul>${v.chapters.map((c: any) => `<li>${escapeHtml(c.t)} &ndash; ${escapeHtml(c.label)}</li>`).join("")}</ul>`
: "";
const ssr = [
'<article class="ssr-content">',
`<h1>${escapeHtml(v.shortTitle)}</h1>`,
`<video controls playsinline preload="metadata" poster="${escapeHtml(v.posterUrl)}" style="max-width:100%;height:auto;" src="${escapeHtml(v.previewUrl)}"></video>`,
`<p><strong>${escapeHtml(desc)}</strong></p>`,
chapterHtml,
`<p><a href="${escapeHtml(v.youtubeUrl)}" rel="noopener">Das ganze Video auf YouTube ansehen</a></p>`,
"</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) {
}
}
const articleMatch = url.match(/^\/article\/([^?#]+)/);
if (articleMatch) {
try {
@@ -318,7 +398,7 @@ export function serveStatic(app: Express) {
// z naslovnico). Seznam mora ustrezati potem v client/src/App.tsx.
const KNOWN = [
/^\/$/, /^\/search\/?$/, /^\/article\//, /^\/category\//, /^\/aktuelles\/?$/,
/^\/videos\/?$/, /^\/gallery\/?$/, /^\/horoskop(\/|$)/, /^\/rezepte\/?$/,
/^\/videos\/?$/, /^\/video\/[^/]+\/?$/, /^\/gallery\/?$/, /^\/horoskop(\/|$)/, /^\/rezepte\/?$/,
/^\/empfang-folx-tv\/?$/, /^\/ueber-uns\/?$/, /^\/impressum\/?$/,
/^\/datenschutz\/?$/, /^\/kontakt\/?$/, /^\/admin(\/|$)/,
];