diff --git a/client/index.html b/client/index.html
index 91e5d6f..5e7ef19 100644
--- a/client/index.html
+++ b/client/index.html
@@ -35,6 +35,32 @@
+
diff --git a/client/src/components/adsense.tsx b/client/src/components/adsense.tsx
index 19d47e6..aa85d3f 100644
--- a/client/src/components/adsense.tsx
+++ b/client/src/components/adsense.tsx
@@ -11,7 +11,6 @@ interface AdSenseProps {
style?: Record;
layout?: string;
layoutKey?: string;
- eager?: boolean;
onAdStatus?: (filled: boolean) => void;
}
@@ -23,11 +22,6 @@ export default function AdSense({
style,
layout,
layoutKey,
- // Privzeto zahtevamo takoj. Odlozena zahteva dvigne vidnost, a znizza stevilo
- // prikazov pri enotah globlje na strani - in prav te (in-article, multiplex)
- // nosijo prihodek. Lazy pot ostaja na voljo prek eager={false}, a je nikjer
- // ne uporabljamo, dokler ucinka ne moremo izmeriti v AdSense.
- eager = true,
onAdStatus,
}: AdSenseProps) {
const adRef = useRef(null);
@@ -35,100 +29,44 @@ export default function AdSense({
useEffect(() => {
if (pushed.current) return;
- const ins = adRef.current;
+ try {
+ const adsbygoogle = (window as any).adsbygoogle || [];
+ adsbygoogle.push({});
+ pushed.current = true;
+ } catch (e) {
+ }
- let io: IntersectionObserver | null = null;
- let obs: MutationObserver | null = null;
- let timer: ReturnType | null = null;
- let decided = false;
+ if (!onAdStatus) return;
+ const ins = adRef.current;
+ if (!ins) return;
// Google postavi data-ad-status na "filled", "unfilled" ali "unfill-optimized".
// Ce statusa ni, se zanesemo na dejansko visino. Odlocimo se enkrat.
- // AdSense odzivni enoti vnaprej doloci visino po sirini zaslona. Ce je
- // postrezena kreativa nizja, ostane pod njo prazen rezerviran pas znotraj
- // okvirja. Visino zato poravnamo na dejansko visino oglasnega okvirja.
- // Nic se ne odreze - visina je natanko taksna kot oglas.
- let fitTimers: ReturnType[] = [];
- const fitToCreative = () => {
- // Enota s trdno visino v style (fiksne Display enote) se ne prilagaja
- // kreativi, sicer nam Google 300x400 razpotegne na 300x600.
- if (style && typeof (style as any).height === "string") return;
- const f = ins?.querySelector("iframe") as HTMLIFrameElement | null;
- if (!ins || !f) return;
- const h = f.getBoundingClientRect().height;
- if (h > 10 && Math.abs(ins.getBoundingClientRect().height - h) > 2) {
- ins.style.height = `${Math.ceil(h)}px`;
- }
- };
- const scheduleFit = () => {
- fitTimers.forEach(clearTimeout);
- fitTimers = [0, 300, 1000, 2500].map((d) => setTimeout(fitToCreative, d));
- };
-
+ let decided = false;
const decide = (filled: boolean) => {
if (decided) return;
decided = true;
- obs?.disconnect();
- if (timer) clearTimeout(timer);
- if (filled) scheduleFit();
- onAdStatus?.(filled);
+ obs.disconnect();
+ 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);
};
-
- // 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 (!ins) return;
- // Opazujemo tudi pri enotah brez onAdStatus - potrebujemo jih za poravnavo
- // visine na kreativo.
- obs = new MutationObserver(() => { check(); fitToCreative(); });
- obs.observe(ins, { attributes: true, childList: true, subtree: true, attributeFilter: ["data-ad-status", "style"] });
- timer = setTimeout(() => decide(hasFrame()), 3000);
- window.addEventListener("resize", scheduleFit);
- check();
- scheduleFit();
- };
-
- 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);
- fitTimers.forEach(clearTimeout);
- window.removeEventListener("resize", scheduleFit);
- };
+ 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); };
}, []);
return (
@@ -139,8 +77,8 @@ export default function AdSense({
style={style || { display: "block" }}
data-ad-client="ca-pub-4465464714854276"
data-ad-slot={slot}
- {...(format ? { "data-ad-format": format } : {})}
- data-full-width-responsive={responsive ? "true" : "false"}
+ data-ad-format={format}
+ data-full-width-responsive={responsive ? "true" : undefined}
{...(layout ? { "data-ad-layout": layout } : {})}
{...(layoutKey ? { "data-ad-layout-key": layoutKey } : {})}
/>
@@ -150,54 +88,20 @@ export default function AdSense({
let adSeedCounter = 1;
-// Vodoravni pas med vrsticami mreze. Oglas v celici je vrstico raztegnil na
-// svojo visino (546 px proti 399 px kartice) in pod izvlecki je ostalo prazno.
-// Pas cez vso sirino na visino kartic ne vpliva in ima boljso zapolnjenost.
-export function GridBannerAd() {
+export function ArticleCardAd() {
+ const seed = useMemo(() => adSeedCounter++, []);
const [filled, setFilled] = useState(null);
+ if (filled === false) return null;
return (
- // Pas cez vso sirino je bil visok 248 do 280 px in je bralca prekinil bolj,
- // kot je prinesel. Zozimo ga na 728 px: pri tej sirini Google servira
- // leaderboard 728x90, na telefonu pa 320x100. Rezanja ne uporabljamo,
- // ker je obrezan oglas krsitev pravil.
-
-
- Anzeige
-
- {/* FOLX 2026 - Leaderboard */}
+
-
- );
-}
-
-export function ArticleCardAd() {
- const [filled, setFilled] = useState(null);
- if (filled === false) return null;
- // 31. 8. 2026: oglas mora biti enak kartici, torej cela celica in ne nizji
- // blok z luknjo pod njim. Okvir je zato h-full in prevzame visino vrstice,
- // oglas pa je v njem centriran. Enota 9916590834 je fiksna 300x400: fiksne
- // mere Googlova skripta ne prevaja, zato se ne razteguje kot responsive.
- return (
-
- {/* FOLX 2026 - Grid, fiksna Display enota 300x600. Fiksna mera pomeni,
- da Googlova skripta visine ne prevaja; stara 9916590834 je bila
- Responsiv in se je razteznila na 548 px. */}
-
-
)}
- {/* format="auto" + full-width-responsive: Google dobi celo sirino in
- izbere kreativo, ki jo zapolni. Pri "horizontal" s trdo visino je
- vracal 320x50 in okrog je ostal prazen rob. */}
- {/* FOLX 2026 - Leaderboard */}
);
}
-// 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
- )}
- {/* Sirina 728 px: pri njej Google servira leaderboard 728x90, na telefonu
- 320x100. Pri polni sirini clanka je servirala velik nativni oglas
- visok 250 px in bralcu je zakrila zacetek besedila. */}
-
-
-
-
- );
-}
-
export function InArticleAd() {
// Prava AdSense In-article enota (fluid + data-ad-layout="in-article").
// Kadar oglasa ni, se okvir in rezerviran prostor ne izriseta.
const [filled, setFilled] = useState(null);
- // 1. 9. 2026: prej se je okvir dodal sele po nalozitvi oglasa in stran je
- // poskocila - meritev je pokazala CLS 0,10 na clanku in 0,15 na naslovnici,
- // Search Console pa 225 naslovov cez prag. Zdaj je prostor rezerviran
- // vnaprej in se po nalozitvi nic ne premakne.
+ if (filled === false) return null;
+ const shown = filled === true;
return (
-
);
}
-// Oba stolpca ostaneta (nic se ne odvzame), a se prilagodita sirini zaslona:
-// 160x600 od 1536 px, 300x600 od 1700 px naprej, kjer je prostora dovolj.
-// Sirino dolocz CSS, AdSense jo prebere ob zahtevi. Dodan onAdStatus, da prazna
-// enota ne pusti luknje - prej je ni imela.
-function SideRail({ side, offset }: { side: "left" | "right"; offset: number }) {
- const [filled, setFilled] = useState(null);
- if (filled === false) return null;
- return (
-
-
- {/* Trdna mera 160x600 (skyscraper). Prej je bil pas na sirokih zaslonih
- 300 px in z responsive vertical si je Google visino racunal sam,
- zato se je raztegoval cez pol zaslona. */}
- {/* FOLX 2026 - Skyscraper, fiksna Display enota 160x600. */}
-
-
-
- );
-}
-
export function PageSideAds({ contentHalfWidth = 640 }: { contentHalfWidth?: number }) {
- // 31. 8. 2026: pasova sta bila prilepljena na rob zaslona, ker je bila
- // spremenljivka --rail-w nad 1700 px se vedno 300, enota pa je zdaj fiksna
- // 160 px - odmik se je racunal s 140 px prevelike sirine. Zdaj je povsod
- // 160, razmik do vsebine pa majhen, da sta pasova tik ob clanku.
- const gap = 8;
+ const gap = 10;
const offset = contentHalfWidth + gap;
return (
-
);
}
+
+// Vodoravni pas med vrsticami mreze na naslovnici. 4. 9. 2026: nazaj na staro
+// enoto 4154017639 (horizontal), enako kot vrhnji pas pred 28. 8.
+export function GridBannerAd() {
+ const [filled, setFilled] = useState(null);
+ if (filled === false) return null;
+ const shown = filled === true;
+ return (
+
- )}
- {/* Vodoravni format brez full-width-responsive: enota se drzi sirine
- svojega vsebnika in da 320x50 ali 320x100. Z vklopljenim
- full-width-responsive je Google vracal 390x390 kvadrat. */}
-
+
+
+
Anzeige
+
diff --git a/client/src/pages/article.tsx b/client/src/pages/article.tsx
index 9841546..58b41ee 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, InContentDisplayAd, PageSideAds, MultiplexAd } from "@/components/adsense";
+import { InArticleAd, PageSideAds, MultiplexAd, TopBannerAd } from "@/components/adsense";
import { useIsMobile } from "@/hooks/use-mobile";
import DOMPurify from "dompurify";
import ShareButtons from "@/components/share-buttons";
@@ -344,8 +344,7 @@ 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. */}
+
- {/* Enota "Folx News - In-Article" (4154017639) je bila prej vrhnji pas nad
- naslovnico. Ostane na clanku, a pod naslovom in tik pred besedilom:
- isti prikazi, boljse mesto, naslov ne pade pod pregib. */}
-
-
+
{(() => {
const sanitized = sanitizeContent(articleBody || article.content);
const blocks = sanitized.split(/(?=<(?:p|h[2-4]|div)[\s>])/i).filter(Boolean);
@@ -435,13 +430,8 @@ export default function ArticlePage() {
// Vsi so fluid in-article enota 7672178246 (najvecji zasluzkar).
const last = segments.length - 1;
- const extra = Math.min(3, Math.max(0, Math.floor((blocks.length - 10) / 5)));
- const adCount = last <= 0 ? 0 : (last === 1 ? 1 : Math.min(2 + extra, last));
- const AD_AFTER: number[] = [];
- for (let i = 0; i < adCount; i++) {
- const pos = i === 0 ? 0 : Math.round((i * last) / adCount);
- if (pos < last && !AD_AFTER.includes(pos)) AD_AFTER.push(pos);
- }
+ // Najvec dva oglasa v besedilu: po prvem odstavku in na sredini (stanje pred 28. 8. 2026).
+ const AD_AFTER = last <= 0 ? [] : (last === 1 ? [0] : [0, Math.floor(last / 2)]);
return (
{
if (!el) return;
@@ -462,9 +452,6 @@ export default function ArticlePage() {
dangerouslySetInnerHTML={{ __html: html }}
data-testid={idx === 0 ? "article-content" : `article-content-${idx}`}
/>
- {/* VSA mesta v besedilu so 7672178246 - to je enota z najvecjim
- prihodkom (98,93 EUR v 7 dneh). Prej sta bili tam dve, zdaj
- dve do pet. Enota 4154017639 ima svoje mesto pred besedilom. */}
{AD_AFTER.includes(idx) && }