From db9eac20e23dad405e3eafe86759d0a237a3fae3 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 28 Aug 2026 09:25:49 +0000 Subject: [PATCH] oglasi: manj vsiljivo in bolje placano MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - vse enote se zahtevajo sele ~200 px pred vstopom v vidno polje (IntersectionObserver); prej so se vse pushale ob nalaganju strani, tudi tiste globoko spodaj, ki jih uporabnik ni nikoli videl. Odlocitev o praznosti (3 s) tece sele od zahteve, ne od mounta. - clanek: zgornji pas odstranjen (potiskal je naslovnico in naslov pod pregib, placan pa je najslabse). Prvi oglas pride sele v besedilu. - clanek: stevilo oglasov vezano na dolzino — 2 osnovna + 1 na vsakih 5 blokov cez 10, strop 5. Mediana (9 blokov) ostane pri 2, dolgi clanki dobijo do 5. Prvi je fluid in-article, vsi nadaljnji nov InContentDisplayAd (odzivni display, visja kreativa). - TopBannerAd na seznamih: odzivni namesto trdih 90 px. - mobilni lepljivi pas: odzivni vodoravni (do 110 px) namesto trdih 320x50, z onAdStatus — prazen se ne izrise. - PageSideAds: en desni 300x600 sticky z onAdStatus namesto dveh 160x600 brez zaznave (prazna je pustila luknjo); prikaze se od 1700 px navzgor. - interstitial odstranjen s horoskopa (prekrivalo cez zaslon, zaklenilo scroll); interstitial-ad.tsx in neuporabljen parallax-ad.tsx izbrisana. CMP ostaja nedotaknjen po dogovoru. --- client/src/components/adsense.tsx | 128 +++++++++++++++------ client/src/components/interstitial-ad.tsx | 75 ------------ client/src/components/mobile-sticky-ad.tsx | 40 ++++--- client/src/components/parallax-ad.tsx | 26 ----- client/src/pages/article.tsx | 20 +++- client/src/pages/horoscope.tsx | 2 - 6 files changed, 135 insertions(+), 156 deletions(-) delete mode 100644 client/src/components/interstitial-ad.tsx delete mode 100644 client/src/components/parallax-ad.tsx diff --git a/client/src/components/adsense.tsx b/client/src/components/adsense.tsx index e9d8b7d..5283cb5 100644 --- a/client/src/components/adsense.tsx +++ b/client/src/components/adsense.tsx @@ -11,6 +11,7 @@ interface AdSenseProps { style?: Record; layout?: string; layoutKey?: string; + eager?: boolean; onAdStatus?: (filled: boolean) => void; } @@ -22,6 +23,7 @@ export default function AdSense({ style, layout, layoutKey, + eager = false, onAdStatus, }: AdSenseProps) { const adRef = useRef(null); @@ -29,44 +31,72 @@ export default function AdSense({ useEffect(() => { if (pushed.current) return; - try { - const adsbygoogle = (window as any).adsbygoogle || []; - adsbygoogle.push({}); - pushed.current = true; - } catch (e) { - } - - if (!onAdStatus) return; const ins = adRef.current; - if (!ins) return; + + let io: IntersectionObserver | null = null; + let obs: MutationObserver | null = null; + let timer: ReturnType | null = null; + let decided = false; // Google postavi data-ad-status na "filled", "unfilled" ali "unfill-optimized". // Ce statusa ni, se zanesemo na dejansko visino. Odlocimo se enkrat. - let decided = false; const decide = (filled: boolean) => { if (decided) return; decided = true; - obs.disconnect(); - clearTimeout(timer); - onAdStatus(filled); + obs?.disconnect(); + if (timer) clearTimeout(timer); + onAdStatus?.(filled); }; // Visina samega ni dokaz - nekatere enote imajo rezerviran prostor. // Napolnjen oglas ima notri okvir (iframe) z dejansko visino. const hasFrame = () => { - const f = ins.querySelector("iframe"); + const f = ins?.querySelector("iframe"); return !!f && (f as HTMLIFrameElement).clientHeight > 10; }; const check = () => { - const st = ins.getAttribute("data-ad-status") || ""; + const st = ins?.getAttribute("data-ad-status") || ""; if (st === "filled") return decide(true); if (st.startsWith("unfill")) return decide(false); if (hasFrame()) return decide(true); }; - const obs = new MutationObserver(check); - obs.observe(ins, { attributes: true, childList: true, subtree: true, attributeFilter: ["data-ad-status", "style"] }); - const timer = setTimeout(() => decide(hasFrame()), 3000); - check(); - return () => { obs.disconnect(); clearTimeout(timer); }; + + // Oglas zahtevamo sele, ko se enota priblizuje vidnemu polju (200 px prej). + // Enote globoko pod pregibom so se prej zahtevale takoj ob nalaganju strani in + // vecine uporabnik ni nikoli videl - nizka vidnost pomeni nizjo ceno. + // Odlocitev o praznosti steje sele od zahteve naprej, ne od mounta. + const push = () => { + if (pushed.current) return; + try { + const adsbygoogle = (window as any).adsbygoogle || []; + adsbygoogle.push({}); + pushed.current = true; + } catch (e) { + } + if (!onAdStatus || !ins) return; + obs = new MutationObserver(check); + obs.observe(ins, { attributes: true, childList: true, subtree: true, attributeFilter: ["data-ad-status", "style"] }); + timer = setTimeout(() => decide(hasFrame()), 3000); + check(); + }; + + if (eager || !ins || typeof IntersectionObserver === "undefined") { + push(); + } else { + io = new IntersectionObserver((entries) => { + if (entries.some((e) => e.isIntersecting)) { + io?.disconnect(); + io = null; + push(); + } + }, { rootMargin: "200px 0px" }); + io.observe(ins); + } + + return () => { + io?.disconnect(); + obs?.disconnect(); + if (timer) clearTimeout(timer); + }; }, []); return ( @@ -123,8 +153,35 @@ export function TopBannerAd() { slot="4154017639" format="horizontal" className="w-full" - style={{ display: "block", width: "100%", height: "90px", textAlign: "center", margin: "0 auto" }} - responsive={false} + style={{ display: "block", width: "100%", textAlign: "center", margin: "0 auto" }} + responsive + eager + onAdStatus={setFilled} + /> + + ); +} + +// Navaden odzivni display v toku besedila. Za razliko od fluid in-article enote +// dobi Google celotno sirino in lahko postrezhe visoko kreativo (336x280, 300x600), +// torej en cel oglas, mimo katerega bralec odskrola - namesto vec drobnih. +export function InContentDisplayAd() { + const [filled, setFilled] = useState(null); + if (filled === false) return null; + const shown = filled === true; + return ( +
+ {shown && ( +
Anzeige
+ )} +
@@ -173,26 +230,29 @@ export function MultiplexAd() { ); } +// En sam desni stolpec 300x600 namesto dveh 160x600. +// 160x600 je stara velikost z malo povprasevanja, dve enoti sta si konkurirali, +// prazna pa je brez onAdStatus pustila luknjo. Prikaze se sele, ko je dovolj +// prostora desno od vsebine (min-[1700px]). export function PageSideAds({ contentHalfWidth = 640 }: { contentHalfWidth?: number }) { - const gap = 10; + const gap = 24; const offset = contentHalfWidth + gap; + const [filled, setFilled] = useState(null); + if (filled === false) return null; return (
-
+
-
-
-
-
-
diff --git a/client/src/components/interstitial-ad.tsx b/client/src/components/interstitial-ad.tsx deleted file mode 100644 index df3a068..0000000 --- a/client/src/components/interstitial-ad.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { useState, useEffect } from "react"; -import { X } from "lucide-react"; -import AdSense from "./adsense"; - -const COUNTER_KEY = "folx-interstitial-count"; -const DELAY_MS = 3000; - -export default function InterstitialAd({ forceShow }: { forceShow?: boolean } = {}) { - const [visible, setVisible] = useState(false); - const [filled, setFilled] = useState(null); - - useEffect(() => { - if (forceShow) { - const timer = setTimeout(() => setVisible(true), DELAY_MS); - return () => clearTimeout(timer); - } - - const raw = sessionStorage.getItem(COUNTER_KEY); - const count = raw ? parseInt(raw, 10) : 0; - const next = count + 1; - sessionStorage.setItem(COUNTER_KEY, next.toString()); - - const shouldShow = next % 2 === 1; - if (!shouldShow) return; - - const timer = setTimeout(() => setVisible(true), DELAY_MS); - return () => clearTimeout(timer); - }, [forceShow]); - - useEffect(() => { - if (visible && filled !== false) { - document.body.style.overflow = "hidden"; - } else { - document.body.style.overflow = ""; - } - return () => { - document.body.style.overflow = ""; - }; - }, [visible, filled]); - - if (!visible || filled === false) return null; - - return ( -
setVisible(false)} - data-testid="interstitial-ad-overlay" - > -
e.stopPropagation()} - data-testid="interstitial-ad-container" - > - -
Anzeige
-
- -
-
-
- ); -} diff --git a/client/src/components/mobile-sticky-ad.tsx b/client/src/components/mobile-sticky-ad.tsx index 4ca4df5..a6cb13b 100644 --- a/client/src/components/mobile-sticky-ad.tsx +++ b/client/src/components/mobile-sticky-ad.tsx @@ -8,6 +8,7 @@ const DISMISS_KEY = "folx-sticky-ad-dismissed"; export default function MobileStickyAd() { const isMobile = useIsMobile(); const [visible, setVisible] = useState(false); + const [filled, setFilled] = useState(null); useEffect(() => { if (!isMobile) return; @@ -23,26 +24,37 @@ export default function MobileStickyAd() { sessionStorage.setItem(DISMISS_KEY, "1"); }; - if (!isMobile || !visible) return null; + if (!isMobile || !visible || filled === false) return null; + const shown = filled === true; return (
-
- -
Anzeige
-
+
+ {shown && ( + + )} + {shown && ( +
Anzeige
+ )} + {/* Odzivna vodoravna enota namesto trdih 320x50: Google lahko postrezhe + tudi 320x100, torej vecjo kreativo tam, kjer je prostor. Visino + omejimo, da sidro ne poje zaslona. */} +
diff --git a/client/src/components/parallax-ad.tsx b/client/src/components/parallax-ad.tsx deleted file mode 100644 index 9cc3449..0000000 --- a/client/src/components/parallax-ad.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { useState } from "react"; -import AdSense from "./adsense"; - -export default function ParallaxAd() { - const [filled, setFilled] = useState(null); - if (filled === false) return null; - const shown = filled === true; - return ( -
-
-
- {shown && ( -
Anzeige
- )} - -
-
- {shown && - ); -} diff --git a/client/src/pages/article.tsx b/client/src/pages/article.tsx index dcefe47..9ead4b8 100644 --- a/client/src/pages/article.tsx +++ b/client/src/pages/article.tsx @@ -9,7 +9,7 @@ import { Button } from "@/components/ui/button"; import { Skeleton } from "@/components/ui/skeleton"; import Header from "@/components/header"; import Footer from "@/components/footer"; -import { InArticleAd, PageSideAds, MultiplexAd, TopBannerAd } from "@/components/adsense"; +import { InArticleAd, InContentDisplayAd, PageSideAds, MultiplexAd } from "@/components/adsense"; import { useIsMobile } from "@/hooks/use-mobile"; import DOMPurify from "dompurify"; import ShareButtons from "@/components/share-buttons"; @@ -304,7 +304,8 @@ export default function ArticlePage() {
- + {/* Zgornjega pasu na clanku namenoma ni: potiskal je naslovnico in naslov + pod pregib, placan pa je najslabse. Prvi oglas pride sele v besedilu. */}
diff --git a/client/src/pages/horoscope.tsx b/client/src/pages/horoscope.tsx index 5a944bf..bd1c0b9 100644 --- a/client/src/pages/horoscope.tsx +++ b/client/src/pages/horoscope.tsx @@ -21,7 +21,6 @@ import { } from "lucide-react"; import Header from "@/components/header"; import Footer from "@/components/footer"; -import InterstitialAd from "@/components/interstitial-ad"; import { InArticleAd, PageSideAds, MultiplexAd } from "@/components/adsense"; import { SIGNS, @@ -385,7 +384,6 @@ export default function HoroscopePage() { return (
-