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:
parent
19acaec48c
commit
590584b893
@ -19,6 +19,7 @@
|
||||
<script async src="https://fundingchoicesmessages.google.com/i/pub-4465464714854276?ers=1" nonce="temp-nonce"></script>
|
||||
<script nonce="temp-nonce">(function() {function signalGooglefcPresent() {if (!window.frames['googlefcPresent']) {if (document.body) {const iframe = document.createElement('iframe'); iframe.style = 'width: 0; height: 0; border: none; z-index: -1000; left: -1000px; top: -1000px;'; iframe.style.display = 'none'; iframe.name = 'googlefcPresent'; document.body.appendChild(iframe);} else {setTimeout(signalGooglefcPresent, 0);}}}signalGooglefcPresent();})();</script>
|
||||
<meta name="description" content="Folx TV - Nummer 1 in Europa für Volksmusik und Schlager. Video Streaming Plattform mit Musik, Unterhaltung und Live-Streaming." />
|
||||
<meta name="keywords" content="Volksmusik, Schlager, Folx TV, Kastelruther Spatzen, Monika Martin, Oswald Sattler, Angela Wiedl, Die Grubertaler, Michael Hirte, Sašo Avsenik, Jonny Hill, Rosanna Rocci, Linda Feller, Julia Buchner, Melanie Payer, Marlena Martinelli, Musikvideo, Streaming, Volksmusik TV" />
|
||||
|
||||
<!-- Google Search Console verification -->
|
||||
<meta name="google-site-verification" content="YcRvFNGnAbAnlhQvhODVJOaqHLVdOKnpCqk7oH-xeKE" />
|
||||
|
||||
@ -11,6 +11,7 @@ import GeschichteLiedPage from "@/pages/GeschichteLiedPage";
|
||||
import GipfelstammtischPage from "@/pages/GipfelstammtischPage";
|
||||
import LivePage from "@/pages/LivePage";
|
||||
import PlayerPage from "@/pages/PlayerPage";
|
||||
import KuenstlerPage from "@/pages/KuenstlerPage";
|
||||
import AdminPage from "@/pages/admin";
|
||||
import PrivacyPolicy from "@/pages/PrivacyPolicy";
|
||||
import TermsOfService from "@/pages/TermsOfService";
|
||||
@ -27,6 +28,7 @@ function Router() {
|
||||
<Route path="/gipfelstammtisch" component={GipfelstammtischPage} />
|
||||
<Route path="/live" component={LivePage} />
|
||||
<Route path="/player" component={PlayerPage} />
|
||||
<Route path="/kuenstler" component={KuenstlerPage} />
|
||||
<Route path="/admin" component={AdminPage} />
|
||||
<Route path="/privacy" component={PrivacyPolicy} />
|
||||
<Route path="/terms" component={TermsOfService} />
|
||||
|
||||
162
client/src/pages/KuenstlerPage.tsx
Normal file
162
client/src/pages/KuenstlerPage.tsx
Normal file
@ -0,0 +1,162 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Link, useLocation } from 'wouter';
|
||||
import { ArrowLeft, Music, Users, Search, Menu, X } from 'lucide-react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import HeaderAd from '@/components/HeaderAd';
|
||||
|
||||
interface Artist {
|
||||
name: string;
|
||||
videoCount: number;
|
||||
}
|
||||
|
||||
export default function KuenstlerPage() {
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
||||
const [, setLocation] = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
document.title = 'Alle Künstler & Interpreten | Folx TV - Video';
|
||||
const metaDescription = document.querySelector('meta[name="description"]');
|
||||
if (metaDescription) {
|
||||
metaDescription.setAttribute('content', 'Entdecken Sie alle Volksmusik- und Schlager-Künstler auf Folx TV. Nummer 1 in Europa für Volksmusik und Schlager.');
|
||||
}
|
||||
}, []);
|
||||
|
||||
const { data, isLoading } = useQuery<{ artists: Artist[], total: number }>({
|
||||
queryKey: ['/api/artists'],
|
||||
});
|
||||
|
||||
const artists = data?.artists || [];
|
||||
const filteredArtists = searchQuery
|
||||
? artists.filter(a => a.name.toLowerCase().includes(searchQuery.toLowerCase()))
|
||||
: artists;
|
||||
|
||||
const letters = Array.from(new Set(filteredArtists.map(a => a.name.charAt(0).toUpperCase()))).sort((a, b) => a.localeCompare(b, 'de'));
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-b from-gray-900 to-black text-white flex items-center justify-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-purple-500"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-b from-gray-900 to-black text-white">
|
||||
<header className="bg-black/80 backdrop-blur-md border-b border-purple-500/30 sticky top-0 z-50">
|
||||
<div className="max-w-7xl mx-auto px-4 py-3 flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<Link href="/">
|
||||
<span className="text-2xl font-bold bg-gradient-to-r from-purple-400 to-blue-400 bg-clip-text text-transparent cursor-pointer">
|
||||
Folx TV
|
||||
</span>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<nav className="hidden md:flex items-center gap-6">
|
||||
<Link href="/" className="text-gray-300 hover:text-white transition-colors">Startseite</Link>
|
||||
<Link href="/folx-stadl" className="text-gray-300 hover:text-white transition-colors">FOLX STADL</Link>
|
||||
<Link href="/geschichte-lied" className="text-gray-300 hover:text-white transition-colors">Geschichte des Liedes</Link>
|
||||
<Link href="/gipfelstammtisch" className="text-gray-300 hover:text-white transition-colors">Gipfelstammtisch</Link>
|
||||
<Link href="/kuenstler" className="text-purple-400 font-semibold">Künstler</Link>
|
||||
<Link href="/live" className="text-red-400 hover:text-red-300 transition-colors flex items-center gap-1">
|
||||
<span className="w-2 h-2 bg-red-500 rounded-full animate-pulse"></span> LIVE
|
||||
</Link>
|
||||
</nav>
|
||||
|
||||
<button className="md:hidden text-white" onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}>
|
||||
{isMobileMenuOpen ? <X size={24} /> : <Menu size={24} />}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{isMobileMenuOpen && (
|
||||
<nav className="md:hidden bg-black/95 border-t border-purple-500/20 py-4 px-4 space-y-3">
|
||||
<Link href="/" className="block text-gray-300 hover:text-white py-2">Startseite</Link>
|
||||
<Link href="/folx-stadl" className="block text-gray-300 hover:text-white py-2">FOLX STADL</Link>
|
||||
<Link href="/geschichte-lied" className="block text-gray-300 hover:text-white py-2">Geschichte des Liedes</Link>
|
||||
<Link href="/gipfelstammtisch" className="block text-gray-300 hover:text-white py-2">Gipfelstammtisch</Link>
|
||||
<Link href="/kuenstler" className="block text-purple-400 font-semibold py-2">Künstler</Link>
|
||||
<Link href="/live" className="block text-red-400 py-2">LIVE</Link>
|
||||
</nav>
|
||||
)}
|
||||
</header>
|
||||
|
||||
<HeaderAd />
|
||||
|
||||
<main className="max-w-7xl mx-auto px-4 py-8">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<Link href="/">
|
||||
<ArrowLeft className="text-gray-400 hover:text-white cursor-pointer" size={24} />
|
||||
</Link>
|
||||
<Users className="text-purple-400" size={28} />
|
||||
<h1 className="text-3xl font-bold">Alle Künstler & Interpreten</h1>
|
||||
<span className="bg-purple-500/20 text-purple-300 px-3 py-1 rounded-full text-sm">
|
||||
{artists.length} Künstler
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="relative mb-8 max-w-md">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" size={18} />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Künstler suchen..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="w-full pl-10 pr-4 py-3 bg-gray-800/50 border border-gray-700 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:border-purple-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2 mb-8">
|
||||
{letters.map(letter => (
|
||||
<a
|
||||
key={letter}
|
||||
href={`#letter-${letter}`}
|
||||
className="w-9 h-9 flex items-center justify-center bg-gray-800 hover:bg-purple-600 rounded-lg text-sm font-bold transition-colors"
|
||||
>
|
||||
{letter}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{letters.map(letter => {
|
||||
const letterArtists = filteredArtists.filter(a => a.name.charAt(0).toUpperCase() === letter);
|
||||
return (
|
||||
<div key={letter} id={`letter-${letter}`} className="mb-8">
|
||||
<h2 className="text-2xl font-bold text-purple-400 mb-4 border-b border-purple-500/30 pb-2">{letter}</h2>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3">
|
||||
{letterArtists.map(artist => (
|
||||
<Link
|
||||
key={artist.name}
|
||||
href={`/?search=${encodeURIComponent(artist.name)}`}
|
||||
className="flex items-center gap-3 p-3 bg-gray-800/40 hover:bg-gray-700/60 rounded-lg transition-colors group"
|
||||
>
|
||||
<div className="w-10 h-10 bg-purple-500/20 rounded-full flex items-center justify-center group-hover:bg-purple-500/40 transition-colors">
|
||||
<Music size={18} className="text-purple-400" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-white font-medium truncate">{artist.name}</p>
|
||||
<p className="text-gray-400 text-sm">{artist.videoCount} Video{artist.videoCount > 1 ? 's' : ''}</p>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{filteredArtists.length === 0 && (
|
||||
<div className="text-center py-16 text-gray-400">
|
||||
<Users size={48} className="mx-auto mb-4 opacity-50" />
|
||||
<p className="text-lg">Keine Künstler gefunden für "{searchQuery}"</p>
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
|
||||
<footer className="bg-black/80 border-t border-gray-800 py-6 mt-12">
|
||||
<div className="max-w-7xl mx-auto px-4 text-center text-gray-500 text-sm">
|
||||
<p>Folx TV - Nummer 1 in Europa für Volksmusik und Schlager</p>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -56,6 +56,13 @@ export default function Home() {
|
||||
if (metaDescription) {
|
||||
metaDescription.setAttribute('content', 'Folx TV - Nummer 1 in Europa für Volksmusik und Schlager. Video Streaming Plattform mit Musik, Unterhaltung und Live-Streaming.');
|
||||
}
|
||||
let metaKeywords = document.querySelector('meta[name="keywords"]');
|
||||
if (!metaKeywords) {
|
||||
metaKeywords = document.createElement('meta');
|
||||
metaKeywords.setAttribute('name', 'keywords');
|
||||
document.head.appendChild(metaKeywords);
|
||||
}
|
||||
metaKeywords.setAttribute('content', 'Volksmusik, Schlager, Folx TV, Kastelruther Spatzen, Monika Martin, Oswald Sattler, Angela Wiedl, Die Grubertaler, Michael Hirte, Sašo Avsenik, Jonny Hill, Rosanna Rocci, Linda Feller, Julia Buchner, Melanie Payer, Marlena Martinelli, Musikvideo, Streaming');
|
||||
}, []);
|
||||
|
||||
// Update videos when new data comes in
|
||||
|
||||
134
server/routes.ts
134
server/routes.ts
@ -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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||
|
||||
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];
|
||||
|
||||
Loading…
Reference in New Issue
Block a user