Add an artist listing page and improve SEO for various pages

Introduces a new `/kuenstler` page, API endpoint `/api/artists`, and enhances meta tags on `index.html` and for crawler requests on `/kuenstler`. Adds `KuenstlerPage.tsx` and updates `routes.ts` and `index.html`.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 401e2ec0-e00d-4f10-9d0e-60f3d479f9a5
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
Replit-Commit-Event-Id: 69ee4fdb-c617-4cdd-b9f6-d3fab268d533
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/60d372ff-2c10-46c7-b01b-10c3435136b0/401e2ec0-e00d-4f10-9d0e-60f3d479f9a5/qFrskyV
This commit is contained in:
sebastjanartic
2026-02-13 17:53:37 +00:00
parent 19acaec48c
commit 590584b893
5 changed files with 304 additions and 2 deletions
+132 -2
View File
@@ -20,6 +20,30 @@ import { setupAuth, isAuthenticated, isAdmin } from "./replitAuth";
import { ObjectStorageService, ObjectNotFoundError } from "./objectStorage";
import { generateVideoDescription, generateBulkDescriptions } from "./aiService";
// Extract unique artist names from video titles
function extractArtists(videos: any[]): { name: string; videoCount: number; videos: any[] }[] {
const artistMap = new Map<string, any[]>();
for (const v of videos) {
const title = v.title || '';
let artist = '';
if (title.includes('–')) {
artist = title.split('–')[0].trim();
} else if (title.includes(' - ')) {
artist = title.split(' - ')[0].trim();
}
if (artist && !artist.toUpperCase().startsWith('FOLX')) {
const normalized = artist.replace(/\s+/g, ' ').trim();
if (!artistMap.has(normalized)) {
artistMap.set(normalized, []);
}
artistMap.get(normalized)!.push(v);
}
}
return Array.from(artistMap.entries())
.map(([name, vids]) => ({ name, videoCount: vids.length, videos: vids }))
.sort((a, b) => a.name.localeCompare(b.name, 'de'));
}
// Find video by short or long ID - moved to top level for export
export async function findVideoByAnyId(id: string) {
try {
@@ -439,6 +463,95 @@ export async function registerRoutes(app: Express): Promise<Server> {
res.send(html);
});
// API endpoint for artists list
app.get('/api/artists', async (req, res) => {
try {
const allVideos = await storage.getVideos(600, 0);
const artists = extractArtists(allVideos);
res.json({ artists: artists.map(a => ({ name: a.name, videoCount: a.videoCount })), total: artists.length });
} catch (error) {
res.status(500).json({ message: 'Error fetching artists' });
}
});
// Server-side meta tags for /kuenstler page (crawlers + SEO)
app.get('/kuenstler', async (req, res, next) => {
const userAgent = req.headers['user-agent']?.toLowerCase() || '';
const crawlers = [
'facebookexternalhit', 'facebot', 'twitterbot', 'whatsapp',
'telegrambot', 'linkedinbot', 'pinterest', 'slackbot',
'viberbot', 'discordbot', 'applebot', 'googlebot',
'bingbot', 'yandex', 'baiduspider', 'duckduckbot'
];
const isCrawler = crawlers.some(crawler => userAgent.includes(crawler));
if (!isCrawler) return next();
const baseUrl = 'https://video.folx.tv';
const pageUrl = `${baseUrl}/kuenstler`;
const allVideos = await storage.getVideos(600, 0);
const artists = extractArtists(allVideos);
const topArtists = artists.slice(0, 30).map(a => a.name).join(', ');
const title = `Alle Künstler & Interpreten (${artists.length}) | Folx TV - Video`;
const description = `Entdecken Sie ${artists.length} Volksmusik- und Schlager-Künstler auf Folx TV: ${topArtists} und viele mehr. Nummer 1 in Europa für Volksmusik und Schlager.`;
const keywords = artists.map(a => a.name).join(', ') + ', Volksmusik, Schlager, Folx TV';
const escapeHtml = (s: string) => s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
const jsonLd = {
"@context": "https://schema.org",
"@type": "CollectionPage",
"name": title,
"description": description,
"url": pageUrl,
"numberOfItems": artists.length,
"publisher": { "@type": "Organization", "name": "FOLX.TV", "url": "https://folx.tv" },
"inLanguage": "de",
"mainEntity": {
"@type": "ItemList",
"numberOfItems": artists.length,
"itemListElement": artists.map((a, i) => ({
"@type": "ListItem",
"position": i + 1,
"item": {
"@type": "MusicGroup",
"name": a.name,
"url": `${pageUrl}#letter-${a.name.charAt(0).toUpperCase()}`
}
}))
}
};
const html = `<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${escapeHtml(title)}</title>
<meta name="description" content="${escapeHtml(description)}">
<meta name="keywords" content="${escapeHtml(keywords)}">
<meta property="og:type" content="website">
<meta property="og:title" content="${escapeHtml(title)}">
<meta property="og:description" content="${escapeHtml(description)}">
<meta property="og:url" content="${pageUrl}">
<meta property="og:site_name" content="Folx TV - Video">
<meta property="og:locale" content="de_DE">
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="${escapeHtml(title)}">
<meta name="twitter:description" content="${escapeHtml(description)}">
<link rel="canonical" href="${pageUrl}">
<script type="application/ld+json">${JSON.stringify(jsonLd)}</script>
</head>
<body>
<h1>Alle Künstler & Interpreten auf Folx TV</h1>
<p>${escapeHtml(description)}</p>
<p>Insgesamt ${artists.length} Künstler mit ${allVideos.length} Musikvideos.</p>
<ul>${artists.map(a => `<li id="${encodeURIComponent(a.name)}"><strong>${escapeHtml(a.name)}</strong> (${a.videoCount} Video${a.videoCount > 1 ? 's' : ''})</li>`).join('')}</ul>
</body>
</html>`;
res.set('Content-Type', 'text/html');
res.send(html);
});
// Server-side meta tags for /folx-stadl page (crawlers + SEO)
app.get('/folx-stadl', async (req, res, next) => {
const userAgent = req.headers['user-agent']?.toLowerCase() || '';
@@ -520,7 +633,7 @@ export async function registerRoutes(app: Express): Promise<Server> {
const pageUrl = `${baseUrl}/geschichte-lied`;
const title = 'Die Geschichte des Liedes - Musikdokumentation | Folx TV - Video';
const description = 'Die Geschichte des Liedes - Entdecken Sie die Entstehung und Hintergründe der beliebtesten Volksmusik- und Schlagerlieder. Eine einzigartige Musikdokumentation auf Folx TV.';
const keywords = 'Geschichte des Liedes, Musikdokumentation, Volksmusik Geschichte, Schlager Historie, Liedgeschichte, Folx TV, Musiksendung';
const keywords = 'Geschichte des Liedes, Musikdokumentation, Volksmusik Geschichte, Schlager Historie, Liedgeschichte, Folx TV, Musiksendung, Kastelruther Spatzen, Monika Martin, Oswald Sattler, Angela Wiedl, Die Grubertaler';
const allVideos = await storage.getVideos(600, 0);
const geschichteVideos = allVideos.filter(v => v.title.includes('Geschichte des Liedes'));
@@ -585,7 +698,7 @@ export async function registerRoutes(app: Express): Promise<Server> {
const pageUrl = `${baseUrl}/gipfelstammtisch`;
const title = 'Gipfelstammtisch - Volksmusik Talkshow | Folx TV - Video';
const description = 'Gipfelstammtisch - Die gemütliche Volksmusik-Talkshow auf Folx TV. Gespräche mit Stars der Volksmusik- und Schlagerszene in einzigartiger Alpenatmosphäre. Jetzt alle Folgen ansehen!';
const keywords = 'Gipfelstammtisch, Volksmusik Talkshow, Schlager Talk, Alpen, Volksmusik Stars, Folx TV, Musiksendung';
const keywords = 'Gipfelstammtisch, Volksmusik Talkshow, Schlager Talk, Alpen, Volksmusik Stars, Folx TV, Musiksendung, Kastelruther Spatzen, Monika Martin, Oswald Sattler, Angela Wiedl, Michael Hirte';
const allVideos = await storage.getVideos(600, 0);
const gipfelVideos = allVideos.filter(v => v.title.toLowerCase().includes('gipfelstammtisch'));
@@ -674,8 +787,25 @@ export async function registerRoutes(app: Express): Promise<Server> {
<changefreq>weekly</changefreq>
<priority>0.7</priority>
</url>
<url>
<loc>${baseUrl}/kuenstler</loc>
<changefreq>weekly</changefreq>
<priority>0.8</priority>
</url>
`;
// Add artist letter-index anchors to sitemap
const artists = extractArtists(videos);
const artistLetters = Array.from(new Set(artists.map(a => a.name.charAt(0).toUpperCase()))).sort();
for (const letter of artistLetters) {
xml += ` <url>
<loc>${baseUrl}/kuenstler#letter-${escapeXml(letter)}</loc>
<changefreq>weekly</changefreq>
<priority>0.6</priority>
</url>
`;
}
for (const video of videos) {
const shortId = video.id.replace(/-/g, '').substring(0, 8);
const lastmod = video.createdAt ? new Date(video.createdAt).toISOString().split('T')[0] : new Date().toISOString().split('T')[0];