From 0e438b1d3c8efa347913db646242c3e4579de671 Mon Sep 17 00:00:00 2001 From: folx bot Date: Mon, 24 Aug 2026 17:11:51 +0200 Subject: [PATCH] =?UTF-8?q?naslovnica:=20/api/articles=20dobi=20limit,=20o?= =?UTF-8?q?ffset=20in=20light=3D1=20(brez=20content=20in=20targetUrl);=20h?= =?UTF-8?q?ome=20nalo=C5=BEi=20prvih=2030=20lahkih=20zapisov,=20ostalo=20d?= =?UTF-8?q?olije=20v=20ozadju;=20deterministi=C4=8Den=20vrstni=20red=20nam?= =?UTF-8?q?esto=20Math.random,=20da=20se=20mre=C5=BEa=20ob=20dolivanju=20n?= =?UTF-8?q?e=20preme=C5=A1a?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/pages/home.tsx | 33 +- client/src/pages/home.tsx.bak | 949 ++++++++++++++++++++++++++++++++++ server/routes.ts | 20 +- 3 files changed, 991 insertions(+), 11 deletions(-) create mode 100644 client/src/pages/home.tsx.bak diff --git a/client/src/pages/home.tsx b/client/src/pages/home.tsx index fcaa75b..3f78c77 100644 --- a/client/src/pages/home.tsx +++ b/client/src/pages/home.tsx @@ -711,9 +711,24 @@ export default function Home() { }; }, []); - const { data: allArticles, isLoading } = useQuery({ - queryKey: ["/api/articles"], + // Prvi paket je majhen in brez polja content, da se naslovnica izrise takoj; + // ostalo se dolije v ozadju. Prej je sla naslovnica po vseh 550 clankov s + // celotnim besedilom (1,77 MB). + const { data: firstBatch, isLoading } = useQuery({ + queryKey: ["/api/articles?limit=30&light=1"], }); + const { data: restBatch } = useQuery({ + queryKey: ["/api/articles?offset=30&light=1"], + enabled: !!firstBatch, + }); + const allArticles = useMemo(() => { + const seen = new Set(); + const out: Article[] = []; + for (const a of [...(firstBatch || []), ...(restBatch || [])]) { + if (!seen.has(a.id)) { seen.add(a.id); out.push(a); } + } + return out; + }, [firstBatch, restBatch]); // Splosne novice imajo svojo rubriko /aktuelles in ne sodijo na naslovnico, // ki ostaja Volksmusik in Schlager. @@ -738,12 +753,14 @@ export default function Home() { const shuffled = useMemo(() => { if (!articles) return []; - const arr = [...articles]; - for (let i = arr.length - 1; i > 0; i--) { - const j = Math.floor(Math.random() * (i + 1)); - [arr[i], arr[j]] = [arr[j], arr[i]]; - } - return arr; + const key = (id: number) => { + let h = id * 2654435761 % 4294967296; + h ^= h >>> 13; h = (h * 1274126177) % 4294967296; + return h; + }; + const head = articles.slice(0, 30).sort((a, b) => key(a.id) - key(b.id)); + const tail = articles.slice(30).sort((a, b) => key(a.id) - key(b.id)); + return [...head, ...tail]; }, [articles]); const widgets = useMemo(() => [ diff --git a/client/src/pages/home.tsx.bak b/client/src/pages/home.tsx.bak new file mode 100644 index 0000000..fcaa75b --- /dev/null +++ b/client/src/pages/home.tsx.bak @@ -0,0 +1,949 @@ +import { useQuery } from "@tanstack/react-query"; +import { Link } from "wouter"; +import { type Article } from "@shared/schema"; +import { format } from "date-fns"; +import { de } from "date-fns/locale"; +import { usePageMeta } from "@/hooks/use-page-meta"; +import { Eye, Play, Images } from "lucide-react"; +import { Skeleton } from "@/components/ui/skeleton"; +import Header from "@/components/header"; +import Footer from "@/components/footer"; +import AdSense, { ArticleCardAd, SidebarAd, PageSideAds } from "@/components/adsense"; +import { PhotoGalleryWidget } from "@/components/photo-gallery"; +import { HoroscopeWidget } from "@/components/horoscope-widget"; +import { RecipeWidget } from "@/components/recipe-widget"; +import { NewsWidget } from "@/components/news-widget"; +import { SidebarWeatherWidget } from "@/components/weather-widget"; +import { BreakingNewsWidget } from "@/components/breaking-news-widget"; +import PushPromptBanner from "@/components/push-prompt-banner"; +import { useState, useEffect, useCallback, useRef, useMemo, lazy, Suspense } from "react"; + +function useFocalPoints() { + const { data } = useQuery>({ + queryKey: ["/api/focal-points"], + staleTime: Infinity, + }); + return data || {}; +} + +function SmartImage({ src, alt, className = "", focalPoints }: { src: string; alt: string; className?: string; focalPoints?: Record }) { + const imgRef = useRef(null); + const [isPortrait, setIsPortrait] = useState(false); + + useEffect(() => { + const img = new window.Image(); + img.onload = () => { + if (img.naturalHeight > img.naturalWidth * 1.2) { + setIsPortrait(true); + } + }; + img.src = src; + }, [src]); + + const fp = focalPoints?.[src]; + const posStyle = fp + ? { objectPosition: `${fp.x}% ${fp.y}%` } + : { objectPosition: "center 20%" }; + + if (isPortrait) { + return ( +
+ + {alt} +
+ ); + } + + return ( + {alt} + ); +} + +interface GalleryImage { + folder: string; + fileName: string; + thumb: string; + large: string; +} + +function thumbUrl(src: string | null): string { + if (!src) return "/images/article-1.jpg"; + if (src.endsWith(".webp")) return src.replace(".webp", "-thumb.webp"); + return src; +} + +function timeAgo(date: Date): string { + const now = new Date(); + const diffMs = now.getTime() - date.getTime(); + const diffH = Math.floor(diffMs / 3600000); + const diffD = Math.floor(diffMs / 86400000); + if (diffH < 1) return "Gerade eben"; + if (diffH < 24) return `vor ${diffH} Std.`; + if (diffD < 7) return `vor ${diffD} T.`; + return format(date, "d. MMM yyyy", { locale: de }); +} + +function HeroCard({ article, focalPoints }: { article: Article; focalPoints?: Record }) { + const isVideo = article.category === "Video"; + return ( + +
+
+ + {isVideo && ( +
+
+ +
+
+ )} +
+
+
+ {article.category} + {timeAgo(new Date(article.publishedAt))} +
+

{article.title}

+

{article.excerpt}

+
+
+
+ + ); +} + +function GalleryHeroCard({ images }: { images: GalleryImage[] }) { + const [idx, setIdx] = useState(0); + + useEffect(() => { + const timer = setInterval(() => setIdx((i) => (i + 1) % images.length), 10000); + return () => clearInterval(timer); + }, [images.length]); + + return ( + +
+
+ {images[idx].fileName} +
+
+
+ + Fotogalerie + +
+

Backstage & Events

+

{images.length} exklusive Fotos aus der Welt der Volksmusik

+
+
+
+ + ); +} + +function getObjectPosition(coverImage: string | null, focalPoints?: Record): string { + if (!coverImage || !focalPoints) return "center 25%"; + const fp = focalPoints[coverImage]; + if (!fp) return "center 25%"; + return `${fp.x}% ${fp.y}%`; +} + +function MosaicTile({ article, focalPoints, tall = false }: { article: Article; focalPoints?: Record; tall?: boolean }) { + const objPos = getObjectPosition(article.coverImage, focalPoints); + return ( + +
+ {article.title} +
+ + {article.category || "News"} + +

+ {article.title} +

+
+ + ); +} + +function MosaicHeading({ children }: { children: React.ReactNode }) { + return ( +
+

{children}

+
+
+
+ ); +} + +function MobileMosaic({ latest, popular, focalPoints }: { latest: Article[]; popular?: Article[]; focalPoints?: Record }) { + const pageSize = 3; + const pool = (latest || []).slice(0, 9); + const totalPages = Math.max(1, Math.ceil(pool.length / pageSize)); + + const [page, setPage] = useState(0); + const [displayPage, setDisplayPage] = useState(0); + const [fading, setFading] = useState(false); + const [paused, setPaused] = useState(false); + + const changePage = useCallback((newPage: number) => { + if (newPage === displayPage) return; + setFading(true); + setTimeout(() => { + setDisplayPage(newPage); + setFading(false); + }, 300); + }, [displayPage]); + + useEffect(() => { + changePage(page); + }, [page]); + + useEffect(() => { + if (paused || totalPages <= 1) return; + const timer = setInterval(() => setPage((p) => (p + 1) % totalPages), 8000); + return () => clearInterval(timer); + }, [paused, totalPages]); + + const touchX = useRef(null); + const resumeTimer = useRef(null); + + const goTo = useCallback((dir: number) => { + setPage((p) => (p + dir + totalPages) % totalPages); + }, [totalPages]); + + const onTouchStart = useCallback((e: React.TouchEvent) => { + touchX.current = e.touches[0].clientX; + setPaused(true); + if (resumeTimer.current) clearTimeout(resumeTimer.current); + }, []); + + const onTouchEnd = useCallback((e: React.TouchEvent) => { + const x0 = touchX.current; + touchX.current = null; + if (x0 !== null) { + const dx = e.changedTouches[0].clientX - x0; + if (Math.abs(dx) > 40) goTo(dx < 0 ? 1 : -1); + } + if (resumeTimer.current) clearTimeout(resumeTimer.current); + resumeTimer.current = setTimeout(() => setPaused(false), 6000); + }, [goTo]); + + useEffect(() => () => { if (resumeTimer.current) clearTimeout(resumeTimer.current); }, []); + + const l = pool.slice(displayPage * pageSize, displayPage * pageSize + pageSize); + if (l.length === 0) return null; + + const usedIds = new Set(pool.map((a) => a.id)); + const p = (popular || []).filter((a) => !usedIds.has(a.id)).slice(0, 3); + + const nextStart = ((displayPage + 1) % totalPages) * pageSize; + const preload = pool.slice(nextStart, nextStart + pageSize); + + return ( +
+
+ Neueste Artikel + {preload.length > 0 && ( + + )} +
+
+ {l.slice(0, 2).map((a) => ( + + ))} +
+ {l[2] && ( +
+ +
+ )} +
+ {totalPages > 1 && ( +
+ {Array.from({ length: totalPages }).map((_, i) => ( +
+ )} +
+ + {p.length > 0 && ( +
+ Beliebt + + {p.length > 1 && ( +
+ {p.slice(1, 3).map((a) => ( + + ))} +
+ )} +
+ )} +
+ ); +} + +function MediumCard({ article, focalPoints }: { article: Article; focalPoints?: Record }) { + const isVideo = article.category === "Video"; + const objPos = getObjectPosition(article.coverImage, focalPoints); + return ( + +
+
+
+ {article.title} +
+ {isVideo && ( +
+
+ +
+
+ )} +
+
+
+ {article.author} + {timeAgo(new Date(article.publishedAt))} +
+

{article.title}

+

{article.excerpt}

+
+ + {article.views.toLocaleString()} +
+
+
+ + ); +} + +function WideCard({ article, focalPoints }: { article: Article; focalPoints?: Record }) { + const isVideo = article.category === "Video"; + const objPos = getObjectPosition(article.coverImage, focalPoints); + return ( + +
+
+ {article.title} + {isVideo && ( +
+
+ +
+
+ )} +
+
+
+ {article.author} + {timeAgo(new Date(article.publishedAt))} +
+

{article.title}

+
+ + {article.views.toLocaleString()} +
+
+
+
+ + ); +} + +function WideCardClassic({ article, focalPoints }: { article: Article; focalPoints?: Record }) { + const isVideo = article.category === "Video"; + const objPos = getObjectPosition(article.coverImage, focalPoints); + return ( + +
+
+
+ {article.title} +
+ {isVideo && ( +
+
+ +
+
+ )} +
+
+
+ {article.author} + {timeAgo(new Date(article.publishedAt))} +
+

{article.title}

+

{article.excerpt}

+
+ + {article.views.toLocaleString()} +
+
+
+ + ); +} + +function BlogCard({ article, focalPoints }: { article: Article; focalPoints?: Record }) { + const isVideo = article.category === "Video"; + const objPos = getObjectPosition(article.coverImage, focalPoints); + return ( + +
+
+
+ {article.title} +
+ {isVideo && ( +
+
+ +
+
+ )} +
+
+
+ {article.author} + {timeAgo(new Date(article.publishedAt))} +
+

{article.title}

+

{article.excerpt}

+
+ + {article.views.toLocaleString()} +
+
+
+ + ); +} + +function SideCard({ article, focalPoints }: { article: Article; focalPoints?: Record }) { + const isVideo = article.category === "Video"; + const objPos = getObjectPosition(article.coverImage, focalPoints); + return ( + +
+
+
+ {article.title} +
+ {isVideo && ( +
+
+ +
+
+ )} +
+
+
+ {article.author} + {timeAgo(new Date(article.publishedAt))} +
+

{article.title}

+
+ + {article.views.toLocaleString()} +
+
+
+ + ); +} + +function NativeAdCard() { + return ( +
+
+
+ +
+
+
+ ); +} + +function TopStoriesList({ articles, className }: { articles: Article[]; className?: string }) { + return ( +
+

+ + Zuletzt hinzugefügt +

+
+ {articles.slice(0, 5).map((article) => ( + +
+

{article.title}

+
+ {article.author} + {timeAgo(new Date(article.publishedAt))} +
+
+ + ))} +
+
+ ); +} + +function FeaturedHeroCard({ article, focalPoints }: { article: Article; focalPoints?: Record }) { + const isVideo = article.category === "Video"; + const objPos = getObjectPosition(article.coverImage, focalPoints); + return ( + +
+
+ {article.title} + {isVideo && ( +
+
+ +
+
+ )} +
+
+
+ {article.category} + {timeAgo(new Date(article.publishedAt))} +
+

{article.title}

+

{article.excerpt}

+
+ + {article.views.toLocaleString()} +
+
+
+ + ); +} + +function FeaturedCarousel({ articles, popular, galleryImages, focalPoints }: { articles: Article[]; popular?: Article[]; galleryImages?: GalleryImage[]; focalPoints?: Record }) { + const pageSize = 3; + const totalPages = Math.max(1, Math.ceil(Math.min(articles.length, 9) / pageSize)); + const [page, setPage] = useState(0); + const [displayPage, setDisplayPage] = useState(0); + const [fading, setFading] = useState(false); + const [paused, setPaused] = useState(false); + + const changePage = useCallback((newPage: number) => { + if (newPage === displayPage) return; + setFading(true); + setTimeout(() => { + setDisplayPage(newPage); + setFading(false); + }, 300); + }, [displayPage]); + + const next = useCallback(() => { + setPage((p) => { + const np = (p + 1) % totalPages; + return np; + }); + }, [totalPages]); + + useEffect(() => { + changePage(page); + }, [page]); + + useEffect(() => { + if (paused || totalPages <= 1) return; + const timer = setInterval(next, 8000); + return () => clearInterval(timer); + }, [paused, next, totalPages]); + + const start = displayPage * pageSize; + const hero = articles[start]; + const sideCards = articles.slice(start + 1, start + 3); + + const nextPageStart = ((displayPage + 1) % totalPages) * pageSize; + const nextPageArticles = articles.slice(nextPageStart, nextPageStart + pageSize); + + return ( +
setPaused(true)} onMouseLeave={() => setPaused(false)}> + {nextPageArticles.length > 0 && ( + + )} +
+
+ {hero && } +
+
+ {sideCards.map((a) => ( + + ))} +
+
+ {totalPages > 1 && ( +
+ {Array.from({ length: totalPages }).map((_, i) => ( +
+ )} +
+ ); +} + +function LazySection({ children, minHeight = "200px" }: { children: JSX.Element; minHeight?: string }) { + const ref = useRef(null); + const [visible, setVisible] = useState(false); + + useEffect(() => { + const el = ref.current; + if (!el) return; + const obs = new IntersectionObserver( + ([entry]) => { + if (entry.isIntersecting) { + setVisible(true); + obs.disconnect(); + } + }, + { rootMargin: "300px" } + ); + obs.observe(el); + return () => obs.disconnect(); + }, []); + + return ( +
+ {visible ? children : null} +
+ ); +} + +function BentoSkeleton() { + return ( +
+
+
+
+ + + + +
+
+
+ ); +} + +export default function Home() { + usePageMeta("Volksmusik & Schlager TV", "FOLX TV – Ihr Fernsehsender Nr. 1 für Volksmusik und Schlager. Aktuelle Nachrichten, Musikvideos, Künstlerporträts, Live-Shows und Interviews aus der Welt der volkstümlichen Musik."); + + useEffect(() => { + const jsonLd = { + "@context": "https://schema.org", + "@type": "WebSite", + "name": "Folx Music Television", + "alternateName": ["FOLX TV", "Volksmusik TV"], + "url": "https://folx.tv", + "description": "FOLX TV – Ihr Fernsehsender Nr. 1 für Volksmusik und Schlager. Aktuelle Nachrichten, Musikvideos, Künstlerporträts, Live-Shows und Interviews aus der Welt der volkstümlichen Musik.", + "publisher": { + "@type": "Organization", + "name": "Folx Music Television", + "url": "https://folx.tv", + "logo": { + "@type": "ImageObject", + "url": `${window.location.origin}/favicon.png` + }, + "sameAs": [ + "https://www.facebook.com/folxtvmusic", + "https://www.instagram.com/folxtv", + "https://www.youtube.com/@FolxTV", + "https://www.tiktok.com/@folx.tv" + ] + }, + "potentialAction": { + "@type": "SearchAction", + "target": "https://folx.tv/search?q={search_term_string}", + "query-input": "required name=search_term_string" + }, + "inLanguage": "de" + }; + const script = document.createElement("script"); + script.type = "application/ld+json"; + script.id = "home-jsonld"; + script.textContent = JSON.stringify(jsonLd); + const existing = document.getElementById("home-jsonld"); + if (existing) existing.remove(); + document.head.appendChild(script); + return () => { + const el = document.getElementById("home-jsonld"); + if (el) el.remove(); + }; + }, []); + + const { data: allArticles, isLoading } = useQuery({ + queryKey: ["/api/articles"], + }); + + // Splosne novice imajo svojo rubriko /aktuelles in ne sodijo na naslovnico, + // ki ostaja Volksmusik in Schlager. + const articles = useMemo( + () => + (allArticles || []).filter( + (a) => !["Aktuelles", "Politik", "Promis", "Sport", "Musik & Film", "Wetter", "Finanzen"].includes(a.category) + ), + [allArticles] + ); + + const { data: popular } = useQuery({ + queryKey: ["/api/articles/popular"], + }); + + const { data: galleryImages } = useQuery({ + queryKey: ["/api/gallery", "home"], + queryFn: () => fetch("/api/gallery?limit=20").then((r) => r.json()), + }); + + const focalPoints = useFocalPoints(); + + const shuffled = useMemo(() => { + if (!articles) return []; + const arr = [...articles]; + for (let i = arr.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [arr[i], arr[j]] = [arr[j], arr[i]]; + } + return arr; + }, [articles]); + + const widgets = useMemo(() => [ + { id: "horoscope", el: }, + { id: "news", el:
}, + { id: "gallery", el: }, + { id: "recipe", el: }, + { id: "breaking", el:
}, + { id: "gallery2", el: }, + { id: "horoscope2", el: }, + { id: "news2", el:
}, + { id: "recipe2", el: }, + { id: "breaking2", el:
}, + ], []); + + const gridItems = useMemo(() => { + const items: { type: "article" | "widget" | "ad"; key: string; article?: Article; widget?: typeof widgets[0] }[] = []; + let ai = 0; + let wi = 0; + const widgetRows = Math.ceil(widgets.length / 2); + + for (let r = 0; r < widgetRows; r++) { + if (wi < widgets.length) items.push({ type: "widget", key: `w-${widgets[wi].id}`, widget: widgets[wi++] }); + if (wi < widgets.length) items.push({ type: "widget", key: `w-${widgets[wi].id}`, widget: widgets[wi++] }); + if (ai < shuffled.length) items.push({ type: "article", key: `a-${shuffled[ai].id}`, article: shuffled[ai++] }); + if (ai < shuffled.length) items.push({ type: "article", key: `a-${shuffled[ai].id}`, article: shuffled[ai++] }); + } + + while (ai < shuffled.length) { + items.push({ type: "article", key: `a-${shuffled[ai].id}`, article: shuffled[ai++] }); + } + + const remainder = items.length % 4; + if (remainder > 0 && shuffled.length > 0) { + let fill = 0; + while (items.length % 4 !== 0) { + const art = shuffled[fill % shuffled.length]; + items.push({ type: "article", key: `fill-${fill}`, article: art }); + fill++; + } + } + + let adCount = 0; + const totalRows = items.length / 4; + for (let r = 0; r < totalRows; r++) { + const rowStart = r * 4; + const hasWidget = items.slice(rowStart, rowStart + 4).some((it) => it.type === "widget"); + if (!hasWidget) { + const pos = adCount % 2 === 0 ? 0 : 3; + items[rowStart + pos] = { type: "ad", key: `ad-row-${adCount++}` }; + } + } + + return items; + }, [shuffled, widgets]); + + const gridRows: (typeof gridItems)[] = useMemo(() => { + const rows: (typeof gridItems)[] = []; + for (let i = 0; i < gridItems.length; i += 4) { + rows.push(gridItems.slice(i, i + 4)); + } + return rows; + }, [gridItems]); + + const widePickedArticles = useMemo(() => { + if (!articles || articles.length < 3) return []; + const carouselIds = new Set(articles.slice(0, 9).map((a) => a.id)); + const candidates = articles.filter((a) => !carouselIds.has(a.id)); + const pool = candidates.length >= 2 ? candidates : articles; + const copy = [...pool]; + for (let i = copy.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [copy[i], copy[j]] = [copy[j], copy[i]]; + } + return copy.slice(0, 2); + }, [articles]); + + const bottomArticles = useMemo(() => { + if (!articles || articles.length === 0) return []; + const carouselIds = new Set(articles.slice(0, 9).map((a) => a.id)); + const nonCarousel = articles.filter((a) => !carouselIds.has(a.id)); + const pool = nonCarousel.length > 0 ? nonCarousel : articles; + const result: Article[] = []; + let idx = 0; + while (result.length < 9) { + result.push(pool[idx % pool.length]); + idx++; + } + return result; + }, [articles]); + + const bottomSection = useMemo(() => { + const items: { type: "widget" | "ad" | "article"; el: JSX.Element }[] = [ + { type: "widget", el: }, + { type: "widget", el: }, + { type: "widget", el: }, + { type: "article", el: }, + { type: "ad", el:
}, + { type: "article", el: }, + { type: "widget", el: }, + { type: "article", el: }, + { type: "article", el: }, + { type: "article", el: }, + { type: "ad", el:
}, + { type: "article", el: }, + ]; + return items; + }, [bottomArticles, focalPoints]); + + if (isLoading || !articles) { + return ( +
+
+
+
+
+ ); + } + + return ( +
+
+ +
+

FOLX TV – Volksmusik & Schlager Fernsehsender: Nachrichten, Videos und Live-Shows

+ +
+
Anzeige
+ +
+ +
+
+
+ a.featured)} popular={popular} galleryImages={galleryImages} focalPoints={focalPoints} /> +
+ new Date(b.publishedAt).getTime() - new Date(a.publishedAt).getTime())} + popular={popular} + focalPoints={focalPoints} + /> +
+
+ + {articles && articles.length > 0 && new Date(b.publishedAt).getTime() - new Date(a.publishedAt).getTime()).slice(0, 5)} />} + +
+
+ + {gridRows.map((row, ri) => ( + +
+
+ {row.map((item) => + item.type === "widget" + ?
{item.widget!.el}
+ : item.type === "ad" + ?
+ : item.article + ? + : null + )} + {ri === gridRows.length - 1 && widePickedArticles.length > 0 && ( +
+ + {widePickedArticles[1] && } +
+ )} +
+
+
+ ))} + + +
+ {bottomSection.map((item, i) => ( +
+ {item.el} +
+ ))} +
+
+ + +
+ + + +
+
+
+ +
+
+ ); +} diff --git a/server/routes.ts b/server/routes.ts index 33dc331..5b761c2 100644 --- a/server/routes.ts +++ b/server/routes.ts @@ -204,9 +204,23 @@ export async function registerRoutes( res.json(getCachedFocalPoints()); }); - app.get("/api/articles", async (_req, res) => { - const articles = await storage.getArticles(); - res.json(articles); + // Naslovnica je do 24.8.2026 potegnila VSEH 550 clankov skupaj s poljem + // content (1,77 MB). Zdaj sprejme limit/offset in light=1, ki odrezeta + // content in targetUrl — za kartico nista potrebna. + app.get("/api/articles", async (req, res) => { + const all = await storage.getArticles(); + const offset = Math.max(0, parseInt(String(req.query.offset ?? "0")) || 0); + const limRaw = parseInt(String(req.query.limit ?? "")); + const lim = Number.isFinite(limRaw) && limRaw > 0 ? limRaw : undefined; + let out: any[] = all.slice(offset, lim ? offset + lim : undefined); + if (String(req.query.light ?? "") === "1") { + out = out.map((a: any) => { + const { content, targetUrl, ...rest } = a; + return rest; + }); + } + res.set("Cache-Control", "public, max-age=60"); + res.json(out); }); app.get("/api/articles/featured", async (_req, res) => {