diff --git a/client/src/lib/focal.ts b/client/src/lib/focal.ts new file mode 100644 index 0000000..c388358 --- /dev/null +++ b/client/src/lib/focal.ts @@ -0,0 +1,30 @@ +import { useQuery } from "@tanstack/react-query"; + +export type FocalPoint = { x: number; y: number }; +export type FocalMap = Record; + +/** + * Fokusne tocke naslovnic (obraz na sliki), ki jih izracuna streznik + * (server/focal-point.ts) in postreze na /api/focal-points. + * Uporabi POVSOD, kjer se naslovnica clanka prikazuje z object-cover, + * sicer brskalnik obreze na sredino in odreze glave. + */ +export function useFocalPoints() { + const { data } = useQuery({ + queryKey: ["/api/focal-points"], + staleTime: 10 * 60 * 1000, + }); + return data; +} + +/** Vrne vrednost za CSS object-position. */ +export function objectPositionFor(coverImage: string | null | undefined, focalPoints?: FocalMap): string { + const FALLBACK = "center 25%"; + if (!coverImage || !focalPoints) return FALLBACK; + const fp = + focalPoints[coverImage] || + focalPoints[coverImage.replace(/^https?:\/\/[^/]+/, "")] || + focalPoints["https://folx.tv" + coverImage]; + if (!fp) return FALLBACK; + return `${fp.x}% ${fp.y}%`; +} diff --git a/client/src/pages/aktuelles.tsx b/client/src/pages/aktuelles.tsx index 6df1463..9c306a9 100644 --- a/client/src/pages/aktuelles.tsx +++ b/client/src/pages/aktuelles.tsx @@ -11,6 +11,7 @@ import Footer from "@/components/footer"; import { PageSideAds, TopBannerAd, MultiplexAd, ArticleCardAd } from "@/components/adsense"; import { format } from "date-fns"; import { de } from "date-fns/locale"; +import { useFocalPoints, objectPositionFor } from "@/lib/focal"; interface Res { articles: Article[]; @@ -25,6 +26,7 @@ const ROW1 = ["Politik", "Promis", "Sport", "Musik & Film"]; const ROW2 = ["Wetter", "Finanzen"]; export default function AktuellesPage() { + const focalPoints = useFocalPoints(); const search = useSearch(); const [, setLocation] = useLocation(); const params = new URLSearchParams(search); @@ -142,7 +144,7 @@ export default function AktuellesPage() { src={a.coverImage} alt={a.title} loading="lazy" - style={{ objectPosition: "center 25%" }} className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-105" + style={{ objectPosition: objectPositionFor(a.coverImage, focalPoints) }} className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-105" /> )} diff --git a/client/src/pages/article.tsx b/client/src/pages/article.tsx index 02a1e15..08a2e1a 100644 --- a/client/src/pages/article.tsx +++ b/client/src/pages/article.tsx @@ -14,6 +14,7 @@ import { useIsMobile } from "@/hooks/use-mobile"; import DOMPurify from "dompurify"; import ShareButtons from "@/components/share-buttons"; import { useEffect, useMemo } from "react"; +import { useFocalPoints, objectPositionFor } from "@/lib/focal"; const ALLOWED_IFRAME_DOMAINS = [ "iframe.mediadelivery.net", @@ -62,6 +63,7 @@ function RelatedArticles({ currentSlug }: { currentSlug: string }) { }); const filtered = articles?.filter((a) => a.slug !== currentSlug); + const focalPoints = useFocalPoints(); if (!filtered || filtered.length === 0) return null; @@ -77,7 +79,7 @@ function RelatedArticles({ currentSlug }: { currentSlug: string }) { src={article.coverImage ? article.coverImage.replace(".webp", "-thumb.webp") : "/images/article-1.jpg"} alt={article.title} className="w-full aspect-video object-cover transition-transform duration-500 group-hover:scale-105" - style={{ objectPosition: "center 25%" }} + style={{ objectPosition: objectPositionFor(article.coverImage, focalPoints) }} loading="lazy" /> @@ -96,6 +98,7 @@ function RelatedArticles({ currentSlug }: { currentSlug: string }) { } export default function ArticlePage() { + const focalPoints = useFocalPoints(); const { slug } = useParams<{ slug: string }>(); const isMobile = useIsMobile(); @@ -313,8 +316,8 @@ export default function ArticlePage() { {article.title}
diff --git a/client/src/pages/category.tsx b/client/src/pages/category.tsx index 1742d8b..919667a 100644 --- a/client/src/pages/category.tsx +++ b/client/src/pages/category.tsx @@ -11,6 +11,7 @@ import Header from "@/components/header"; import Footer from "@/components/footer"; import { ArticleCardAd, PageSideAds, TopBannerAd, MultiplexAd } from "@/components/adsense"; import { useEffect } from "react"; +import { useFocalPoints, objectPositionFor } from "@/lib/focal"; interface PaginatedResponse { articles: Article[]; @@ -21,6 +22,7 @@ interface PaginatedResponse { } export default function CategoryPage({ fixedCategory }: { fixedCategory?: string } = {}) { + const focalPoints = useFocalPoints(); const params = useParams<{ category: string }>(); const category = fixedCategory || params.category; const searchString = useSearch(); @@ -175,7 +177,7 @@ export default function CategoryPage({ fixedCategory }: { fixedCategory?: string src={a.coverImage || "/images/article-1.jpg"} alt={a.title} className="w-full aspect-video object-cover transition-transform duration-500 group-hover:scale-105" - style={{ objectPosition: "center 25%" }} + style={{ objectPosition: objectPositionFor(a.coverImage, focalPoints) }} loading="lazy" />
diff --git a/client/src/pages/search.tsx b/client/src/pages/search.tsx index 5aca8b5..2b4294b 100644 --- a/client/src/pages/search.tsx +++ b/client/src/pages/search.tsx @@ -5,6 +5,7 @@ import { Search, Play, FileText, ArrowLeft } from "lucide-react"; import { usePageMeta } from "@/hooks/use-page-meta"; import Header from "@/components/header"; import { PageSideAds, TopBannerAd } from "@/components/adsense"; +import { useFocalPoints, objectPositionFor } from "@/lib/focal"; interface SearchResult { articles: { @@ -27,6 +28,7 @@ interface SearchResult { } export default function SearchPage() { + const focalPoints = useFocalPoints(); usePageMeta("Suche - Volksmusik & Schlager", "Durchsuchen Sie FOLX TV nach Volksmusik- und Schlager-Nachrichten, Künstlern, Videos und mehr."); const searchString = useSearch(); const initialQuery = new URLSearchParams(searchString).get("q") || ""; @@ -130,7 +132,7 @@ export default function SearchPage() { {article.title} )}
@@ -165,7 +167,7 @@ export default function SearchPage() { {video.title}
diff --git a/client/src/pages/videos.tsx b/client/src/pages/videos.tsx index 90f48fb..628c37d 100644 --- a/client/src/pages/videos.tsx +++ b/client/src/pages/videos.tsx @@ -11,6 +11,7 @@ import Header from "@/components/header"; import Footer from "@/components/footer"; import { ArticleCardAd, PageSideAds, TopBannerAd, MultiplexAd } from "@/components/adsense"; import { useEffect } from "react"; +import { useFocalPoints, objectPositionFor } from "@/lib/focal"; interface PaginatedResponse { articles: Article[]; @@ -24,6 +25,7 @@ function VideoCard({ article }: { article: Article }) { const thumbSrc = article.coverImage ? article.coverImage.replace(".webp", "-thumb.webp") : "/images/article-1.jpg"; + const focalPoints = useFocalPoints(); return ( @@ -36,7 +38,7 @@ function VideoCard({ article }: { article: Article }) { {article.title}
diff --git a/server/routes.ts b/server/routes.ts index b573925..d2607ea 100644 --- a/server/routes.ts +++ b/server/routes.ts @@ -75,11 +75,18 @@ export async function registerRoutes( return res.status(401).json({ message: "Unauthorized" }); }; - storage.getArticles().then((articles) => { - analyzeAllArticleImages(articles).catch(err => - console.log("[focal-point] Analysis skipped:", err.message) - ); - }); + // Fokusne tocke naslovnic (kje je obraz) — brez njih object-cover obreze na + // sredino in odreze glave. Racunamo ob zagonu IN vsakih 20 minut, da jih + // dobijo tudi clanki, ki jih boti objavijo med tem. + const refreshFocalPoints = () => { + storage.getArticles().then((articles) => { + analyzeAllArticleImages(articles).catch(err => + console.log("[focal-point] Analysis skipped:", err.message) + ); + }).catch(err => console.log("[focal-point] getArticles failed:", err?.message)); + }; + refreshFocalPoints(); + setInterval(refreshFocalPoints, 20 * 60 * 1000); // ---- SEO: robots.txt, sitemap.xml, news sitemap, RSS ---- // Rubrike z onemusic.news imajo isto besedilo kot tam, zato jih ne oddajamo