refactor(components): refactor Ezoic ad and placeholder components

Entire-Checkpoint: afbec17cde9f
This commit is contained in:
Fatih Kadir Akın
2026-03-20 01:12:46 +03:00
parent 4ef6246284
commit 252bcb5de3
3 changed files with 25 additions and 29 deletions
+3 -14
View File
@@ -1,6 +1,5 @@
"use client";
import { useEffect, useState } from "react";
import { EzoicPlaceholder } from "./ezoic-placeholder";
interface EzoicAdProps {
@@ -8,20 +7,10 @@ interface EzoicAdProps {
}
/**
* Client-only wrapper that defers EzoicPlaceholder mounting until after
* hydration. This matches the feed widget pattern where InfinitePromptList
* gates widget rendering behind an isMounted state, ensuring EzoicPlaceholder
* mounts in a separate render pass — after EzoicAds route handler has
* completed its initial destroyPlaceholders/showAds cycle.
* Client-only wrapper for EzoicPlaceholder.
* The placeholder div is always rendered so it's in the DOM when
* Ezoic's showAds() runs — no deferred mounting needed.
*/
export function EzoicAd({ id }: EzoicAdProps) {
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) return null;
return <EzoicPlaceholder id={id} />;
}
+7 -7
View File
@@ -10,15 +10,16 @@ interface EzoicPlaceholderProps {
export function EzoicPlaceholder({ id }: EzoicPlaceholderProps) {
const t = useTranslations("common");
const [isRendered, setIsRendered] = useState(false);
const [hasAdContent, setHasAdContent] = useState(true);
const placeholderRef = useRef<HTMLDivElement>(null);
useEffect(() => {
// Per Ezoic Next.js docs: render the placeholder div first (via state),
// then call showAds(id). The global EzoicRouteHandler's showAds() call
// acts as a safety net to catch all placeholders.
setIsRendered(true);
// The placeholder div is always rendered (no conditional gating),
// so it's in the DOM when this effect runs. Call showAds(id) to
// tell Ezoic to fill it. The global EzoicAds route handler's
// showAds() call acts as an additional safety net.
const el = placeholderRef.current;
runEzoic(() => {
window.ezstandalone?.showAds(id);
});
@@ -26,7 +27,6 @@ export function EzoicPlaceholder({ id }: EzoicPlaceholderProps) {
// Observe the placeholder for ad content injection by Ezoic.
// If nothing is injected after a timeout, hide the container
// to avoid showing an empty white card.
const el = document.getElementById(`ezoic-pub-ad-placeholder-${id}`);
let observer: MutationObserver | undefined;
let timeoutId: ReturnType<typeof setTimeout> | undefined;
@@ -69,7 +69,7 @@ export function EzoicPlaceholder({ id }: EzoicPlaceholderProps) {
</span>
</div>
<div className="ezoic-ad-content overflow-hidden flex items-center justify-center">
{isRendered && <div id={`ezoic-pub-ad-placeholder-${id}`} ref={placeholderRef} />}
<div id={`ezoic-pub-ad-placeholder-${id}`} ref={placeholderRef} />
</div>
</div>
);
+15 -8
View File
@@ -25,10 +25,15 @@ export function EzoicAds() {
useEffect(() => {
runEzoic(() => {
window.ezstandalone?.destroyPlaceholders();
requestAnimationFrame(() => {
});
// Delay the global showAds() to give React time to render all
// placeholder divs into the DOM after route change.
const timer = setTimeout(() => {
runEzoic(() => {
window.ezstandalone?.showAds();
});
});
}, 100);
return () => clearTimeout(timer);
}, [pathname]);
return (
@@ -47,18 +52,20 @@ export function EzoicAds() {
data-cfasync="false"
/>
{/* Ezoic init — must run before sa.min.js so the cmd queue exists */}
<Script id="ezoic-init" strategy="beforeInteractive">
{`
window.ezstandalone = window.ezstandalone || {};
window.ezstandalone.cmd = window.ezstandalone.cmd || [];
`}
</Script>
{/* Ezoic Header Script */}
<Script
id="ezoic-sa"
src="//www.ezojs.com/ezoic/sa.min.js"
strategy="afterInteractive"
/>
<Script id="ezoic-init" strategy="afterInteractive">
{`
window.ezstandalone = window.ezstandalone || {};
window.ezstandalone.cmd = window.ezstandalone.cmd || [];
`}
</Script>
{/* Ezoic Analytics */}
<Script