Add exclusive photos from recordings to the gallery and improve site metadata
Integrate AdSense ads into the photo gallery, introduce SSR meta tags for the gallery page, and improve layout stability by reserving space for ads and dynamic content. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 23852c00-4779-460a-9e0c-d09fee4b6c92 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 81f3d8bc-2328-4374-a06e-57257dbb713b Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/f209e72a-0939-48fa-84fc-57854de71967/23852c00-4779-460a-9e0c-d09fee4b6c92/OPD8Ro3 Replit-Helium-Checkpoint-Created: true
This commit is contained in:
parent
8a263dee0f
commit
4ece9e4ccd
@ -96,7 +96,7 @@ export function ArticleCardAd() {
|
|||||||
|
|
||||||
export function InArticleAd() {
|
export function InArticleAd() {
|
||||||
return (
|
return (
|
||||||
<div className="my-6 rounded-md overflow-hidden bg-card border border-card-border">
|
<div className="my-6 rounded-md overflow-hidden bg-card border border-card-border" style={{ minHeight: "106px" }}>
|
||||||
<div className="text-[9px] text-muted-foreground/40 text-center pt-1 uppercase tracking-widest">Anzeige</div>
|
<div className="text-[9px] text-muted-foreground/40 text-center pt-1 uppercase tracking-widest">Anzeige</div>
|
||||||
<AdSense
|
<AdSense
|
||||||
slot="4154017639"
|
slot="4154017639"
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { useState, useEffect, useCallback, useRef } from "react";
|
import { useState, useEffect, useCallback, useRef } from "react";
|
||||||
import { ChevronLeft, ChevronRight, X, Images, Maximize2 } from "lucide-react";
|
import { ChevronLeft, ChevronRight, X, Images, Maximize2 } from "lucide-react";
|
||||||
|
import { InArticleAd } from "./adsense";
|
||||||
|
|
||||||
export interface GalleryImage {
|
export interface GalleryImage {
|
||||||
folder: string;
|
folder: string;
|
||||||
@ -340,13 +341,24 @@ export default function GalleryPage() {
|
|||||||
<p className="text-muted-foreground text-sm mb-4" data-testid="text-gallery-count">
|
<p className="text-muted-foreground text-sm mb-4" data-testid="text-gallery-count">
|
||||||
{Math.min(visibleCount, totalCount)} von {totalCount} Fotos
|
{Math.min(visibleCount, totalCount)} von {totalCount} Fotos
|
||||||
</p>
|
</p>
|
||||||
|
{(() => {
|
||||||
|
const AD_EVERY = 12;
|
||||||
|
const chunks: GalleryImage[][] = [];
|
||||||
|
for (let i = 0; i < visibleImages.length; i += AD_EVERY) {
|
||||||
|
chunks.push(visibleImages.slice(i, i + AD_EVERY));
|
||||||
|
}
|
||||||
|
return chunks.map((chunk, ci) => (
|
||||||
|
<div key={ci}>
|
||||||
|
{ci > 0 && <InArticleAd />}
|
||||||
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-3">
|
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-3">
|
||||||
{visibleImages.map((img, i) => (
|
{chunk.map((img, j) => {
|
||||||
|
const globalIdx = ci * AD_EVERY + j;
|
||||||
|
return (
|
||||||
<button
|
<button
|
||||||
key={`${img.folder}-${img.fileName}`}
|
key={`${img.folder}-${img.fileName}`}
|
||||||
onClick={() => setLightboxIndex(i)}
|
onClick={() => setLightboxIndex(globalIdx)}
|
||||||
className="group relative rounded-lg overflow-hidden bg-card border border-card-border cursor-pointer flex flex-col"
|
className="group relative rounded-lg overflow-hidden bg-card border border-card-border cursor-pointer flex flex-col"
|
||||||
data-testid={`button-gallery-image-${i}`}
|
data-testid={`button-gallery-image-${globalIdx}`}
|
||||||
>
|
>
|
||||||
<div className="relative w-full aspect-[16/9]">
|
<div className="relative w-full aspect-[16/9]">
|
||||||
<LazyImage
|
<LazyImage
|
||||||
@ -358,7 +370,7 @@ export default function GalleryPage() {
|
|||||||
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/20 transition-colors" />
|
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/20 transition-colors" />
|
||||||
{img.artist && (
|
{img.artist && (
|
||||||
<div className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/70 to-transparent p-2 hidden md:block opacity-0 group-hover:opacity-100 transition-opacity">
|
<div className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/70 to-transparent p-2 hidden md:block opacity-0 group-hover:opacity-100 transition-opacity">
|
||||||
<span className="text-white text-xs font-medium drop-shadow-lg" data-testid={`text-artist-desktop-${i}`}>
|
<span className="text-white text-xs font-medium drop-shadow-lg" data-testid={`text-artist-desktop-${globalIdx}`}>
|
||||||
{img.artist}
|
{img.artist}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@ -369,14 +381,18 @@ export default function GalleryPage() {
|
|||||||
</div>
|
</div>
|
||||||
{img.artist && (
|
{img.artist && (
|
||||||
<div className="md:hidden px-2 py-1.5 bg-card border-t border-card-border">
|
<div className="md:hidden px-2 py-1.5 bg-card border-t border-card-border">
|
||||||
<span className="text-[11px] text-card-foreground font-medium line-clamp-1" data-testid={`text-artist-mobile-${i}`}>
|
<span className="text-[11px] text-card-foreground font-medium line-clamp-1" data-testid={`text-artist-mobile-${globalIdx}`}>
|
||||||
{img.artist}
|
{img.artist}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
));
|
||||||
|
})()}
|
||||||
|
|
||||||
{hasMore && (
|
{hasMore && (
|
||||||
<div ref={sentinelRef} className="flex justify-center py-8" data-testid="gallery-load-more">
|
<div ref={sentinelRef} className="flex justify-center py-8" data-testid="gallery-load-more">
|
||||||
|
|||||||
@ -303,3 +303,12 @@ div[class^="fc-"] {
|
|||||||
inset: -1px;
|
inset: -1px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ad-container {
|
||||||
|
contain: layout style;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.adsbygoogle {
|
||||||
|
contain: layout style;
|
||||||
|
}
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import { usePageMeta } from "@/hooks/use-page-meta";
|
|||||||
import { InArticleAd, PageSideAds } from "@/components/adsense";
|
import { InArticleAd, PageSideAds } from "@/components/adsense";
|
||||||
|
|
||||||
export default function GalleryPageWrapper() {
|
export default function GalleryPageWrapper() {
|
||||||
usePageMeta("Fotogalerie - Volksmusik & Schlager Bilder", "Exklusive Fotos und Bilder von Volksmusik- und Schlager-Stars, Konzerten und Events. Die Volksmusik-Fotogalerie von FOLX TV.");
|
usePageMeta("Fotogalerie – Exklusive Backstage- & Konzertfotos | FOLX TV", "Einzigartige Fotos direkt von unseren Aufzeichnungen und Sendungen: Backstage-Momente, Konzertbilder und exklusive Aufnahmen der Volksmusik- und Schlager-Stars bei FOLX TV.");
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-background">
|
<div className="min-h-screen bg-background">
|
||||||
<Header />
|
<Header />
|
||||||
|
|||||||
@ -276,6 +276,7 @@ function SignDetail({ signIndex, onNavigate, aiHoroscopes }: { signIndex: number
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div style={{ minHeight: "180px" }}>
|
||||||
{tab === "daily" && (
|
{tab === "daily" && (
|
||||||
<div className="space-y-5" data-testid="section-horoscope-daily">
|
<div className="space-y-5" data-testid="section-horoscope-daily">
|
||||||
<div>
|
<div>
|
||||||
@ -311,6 +312,7 @@ function SignDetail({ signIndex, onNavigate, aiHoroscopes }: { signIndex: number
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<InArticleAd />
|
<InArticleAd />
|
||||||
|
|
||||||
|
|||||||
@ -7,6 +7,7 @@ The official website for Folx Music Television (folx.tv). Dark-themed bento grid
|
|||||||
- Primary keyword: "Volksmusik" — used across all page titles, meta descriptions, OG tags, and structured data
|
- Primary keyword: "Volksmusik" — used across all page titles, meta descriptions, OG tags, and structured data
|
||||||
- Dynamic canonical URLs via `usePageMeta` hook (updates `<link rel="canonical">` per page)
|
- Dynamic canonical URLs via `usePageMeta` hook (updates `<link rel="canonical">` per page)
|
||||||
- SSR article pages: server-side meta tags (OG, Twitter, description, keywords, canonical) in both `server/vite.ts` (dev) and `server/static.ts` (prod)
|
- SSR article pages: server-side meta tags (OG, Twitter, description, keywords, canonical) in both `server/vite.ts` (dev) and `server/static.ts` (prod)
|
||||||
|
- SSR gallery page (`/gallery`): OG meta, Twitter card, description, canonical — emphasizes exclusive backstage/concert photos from recordings
|
||||||
- `stripExistingMeta()` removes duplicate meta/canonical from base HTML before injecting article-specific ones
|
- `stripExistingMeta()` removes duplicate meta/canonical from base HTML before injecting article-specific ones
|
||||||
- JSON-LD structured data: WebSite (home) with SearchAction, NewsArticle + BreadcrumbList (articles)
|
- JSON-LD structured data: WebSite (home) with SearchAction, NewsArticle + BreadcrumbList (articles)
|
||||||
- Sitemap at `/sitemap.xml` — includes all static pages, categories, horoscope signs, and articles
|
- Sitemap at `/sitemap.xml` — includes all static pages, categories, horoscope signs, and articles
|
||||||
|
|||||||
@ -95,6 +95,41 @@ export function serveStatic(app: Express) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (url.match(/^\/gallery(\/|$|\?)/)) {
|
||||||
|
let template = await fs.promises.readFile(indexPath, "utf-8");
|
||||||
|
template = stripExistingMeta(template);
|
||||||
|
const galTitle = "Fotogalerie \u2013 Exklusive Backstage- & Konzertfotos | FOLX TV";
|
||||||
|
const galDesc = "Einzigartige Fotos direkt von unseren Aufzeichnungen und Sendungen: Backstage-Momente, Konzertbilder und exklusive Aufnahmen der Volksmusik- und Schlager-Stars bei FOLX TV.";
|
||||||
|
const galUrl = `${canonicalBase}/gallery`;
|
||||||
|
const galImage = `${canonicalBase}/images/og-image.jpg`;
|
||||||
|
|
||||||
|
const galTags = [
|
||||||
|
`<meta property="og:title" content="${escapeHtml(galTitle)}" />`,
|
||||||
|
`<meta property="og:description" content="${escapeHtml(galDesc)}" />`,
|
||||||
|
`<meta property="og:type" content="website" />`,
|
||||||
|
`<meta property="og:url" content="${escapeHtml(galUrl)}" />`,
|
||||||
|
`<meta property="og:image" content="${escapeHtml(galImage)}" />`,
|
||||||
|
`<meta property="og:image:secure_url" content="${escapeHtml(galImage)}" />`,
|
||||||
|
`<meta property="og:image:width" content="1200" />`,
|
||||||
|
`<meta property="og:image:height" content="630" />`,
|
||||||
|
`<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(galTitle)}" />`,
|
||||||
|
`<meta name="twitter:description" content="${escapeHtml(galDesc)}" />`,
|
||||||
|
`<meta name="twitter:image" content="${escapeHtml(galImage)}" />`,
|
||||||
|
`<meta name="description" content="${escapeHtml(galDesc)}" />`,
|
||||||
|
`<meta name="keywords" content="Fotogalerie, Volksmusik Fotos, Schlager Bilder, Konzertfotos, Backstage, FOLX TV, Volksmusik Stars" />`,
|
||||||
|
`<link rel="canonical" href="${escapeHtml(galUrl)}" />`,
|
||||||
|
`<title>${escapeHtml(galTitle)}</title>`,
|
||||||
|
].join("\n ");
|
||||||
|
|
||||||
|
template = template.replace(/<title>[^<]*<\/title>/, galTags);
|
||||||
|
res.status(200).set({ "Content-Type": "text/html" }).end(template);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (url.match(/^\/horoskop(\/|$|\?)/)) {
|
if (url.match(/^\/horoskop(\/|$|\?)/)) {
|
||||||
let template = await fs.promises.readFile(indexPath, "utf-8");
|
let template = await fs.promises.readFile(indexPath, "utf-8");
|
||||||
template = stripExistingMeta(template);
|
template = stripExistingMeta(template);
|
||||||
|
|||||||
@ -113,6 +113,38 @@ export async function setupVite(server: Server, app: Express) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (url.match(/^\/gallery(\/|$|\?)/)) {
|
||||||
|
template = stripExistingMeta(template);
|
||||||
|
const galTitle = "Fotogalerie \u2013 Exklusive Backstage- & Konzertfotos | FOLX TV";
|
||||||
|
const galDesc = "Einzigartige Fotos direkt von unseren Aufzeichnungen und Sendungen: Backstage-Momente, Konzertbilder und exklusive Aufnahmen der Volksmusik- und Schlager-Stars bei FOLX TV.";
|
||||||
|
const galUrl = `${canonicalBase}/gallery`;
|
||||||
|
const galImage = `${canonicalBase}/images/og-image.jpg`;
|
||||||
|
|
||||||
|
const galTags = [
|
||||||
|
`<meta property="og:title" content="${escapeHtml(galTitle)}" />`,
|
||||||
|
`<meta property="og:description" content="${escapeHtml(galDesc)}" />`,
|
||||||
|
`<meta property="og:type" content="website" />`,
|
||||||
|
`<meta property="og:url" content="${escapeHtml(galUrl)}" />`,
|
||||||
|
`<meta property="og:image" content="${escapeHtml(galImage)}" />`,
|
||||||
|
`<meta property="og:image:secure_url" content="${escapeHtml(galImage)}" />`,
|
||||||
|
`<meta property="og:image:width" content="1200" />`,
|
||||||
|
`<meta property="og:image:height" content="630" />`,
|
||||||
|
`<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(galTitle)}" />`,
|
||||||
|
`<meta name="twitter:description" content="${escapeHtml(galDesc)}" />`,
|
||||||
|
`<meta name="twitter:image" content="${escapeHtml(galImage)}" />`,
|
||||||
|
`<meta name="description" content="${escapeHtml(galDesc)}" />`,
|
||||||
|
`<meta name="keywords" content="Fotogalerie, Volksmusik Fotos, Schlager Bilder, Konzertfotos, Backstage, FOLX TV, Volksmusik Stars" />`,
|
||||||
|
`<link rel="canonical" href="${escapeHtml(galUrl)}" />`,
|
||||||
|
`<title>${escapeHtml(galTitle)}</title>`,
|
||||||
|
].join("\n ");
|
||||||
|
|
||||||
|
template = template.replace(/<title>[^<]*<\/title>/, galTags);
|
||||||
|
}
|
||||||
|
|
||||||
if (url.match(/^\/horoskop(\/|$|\?)/)) {
|
if (url.match(/^\/horoskop(\/|$|\?)/)) {
|
||||||
template = stripExistingMeta(template);
|
template = stripExistingMeta(template);
|
||||||
const signMatch = url.match(/^\/horoskop\/([^?#]+)/);
|
const signMatch = url.match(/^\/horoskop\/([^?#]+)/);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user