Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions apps/site/app/_components/ad-unit.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
"use client";

import { useEffect, useRef } from "react";
import { usePathname } from "next/navigation";

/**
* One CrawlProof ad unit, above the footer on public pages.
* Manage the slot at https://crawlproof.com/ads/slots
*/
const AD_SLOT = "5500e925-8ab1-4dd9-b89d-eaafa3554f68";

/** Account, auth and checkout pages carry no ad. */
const EXCLUDED_PREFIXES = [
"/account",
"/checkout",
"/library",
"/orders",
"/login",
"/register",
"/forgot-password",
"/reset-password",
"/verify-email",
"/play",
];

/** 728x90 needs the full row; anything narrower gets the 300x250 rectangle. */
const LEADERBOARD_QUERY = "(min-width: 768px)";

const SCAN_RETRY_MS = 200;
const SCAN_MAX_TRIES = 25;

declare global {
interface Window {
crawlproofAds?: { scan: () => void };
}
}

function isExcluded(pathname: string): boolean {
return EXCLUDED_PREFIXES.some((p) => pathname === p || pathname.startsWith(`${p}/`));
}

export function AdUnit() {
const pathname = usePathname() ?? "/";
if (isExcluded(pathname)) return null;
// Keyed by path so each client-side navigation gets a fresh unit: ad.js
// marks a filled element and never refills it in place.
return <AdSlot key={pathname} />;
}

function AdSlot() {
const ref = useRef<HTMLDivElement>(null);

useEffect(() => {
const el = ref.current;
if (!el) return;
// The server renders the element without data-slot, so ad.js's own
// DOMContentLoaded pass skips it; the format is chosen here from the real
// viewport and the unit fills exactly once.
el.dataset.format = window.matchMedia(LEADERBOARD_QUERY).matches
? "banner_728x90"
: "banner_300x250";
el.dataset.slot = AD_SLOT;

// ad.js scans once at DOMContentLoaded and may not have loaded yet, so
// retry briefly; give up after ~5s so an ad blocker leaves no timer.
let tries = 0;
let timer: ReturnType<typeof setTimeout> | undefined;
const tick = () => {
if (window.crawlproofAds) {
window.crawlproofAds.scan();
return;
}
if (++tries > SCAN_MAX_TRIES) return;
timer = setTimeout(tick, SCAN_RETRY_MS);
};
tick();
return () => {
if (timer) clearTimeout(timer);
};
}, []);

return <div ref={ref} data-cp-ad="" className="cp-ad" />;
}
28 changes: 28 additions & 0 deletions apps/site/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -1759,3 +1759,31 @@ a {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}

/*
* CrawlProof ad unit: zero height until ad.js starts a fill (it sets
* data-cp-filled, and removes it again on an empty response), so an unsold or
* blocked slot leaves no hole. The reserved height is the format plus the
* "Advertisement" caption ad.js draws above banners.
*/
.cp-ad {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
height: 0;
overflow: hidden;
}

.cp-ad[data-cp-filled] {
height: auto;
margin: 2rem 0;
}

.cp-ad[data-cp-filled][data-format="banner_728x90"] {
min-height: 106px;
}

.cp-ad[data-cp-filled][data-format="banner_300x250"] {
min-height: 266px;
}
3 changes: 3 additions & 0 deletions apps/site/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { Metadata, Viewport } from "next";
import "./globals.css";
import Script from "next/script";
import { Footer } from "@profullstack/footer/react";
import { AdUnit } from "./_components/ad-unit";
import captures from "../public/media/yard/manifest.json";
import trailer from "../public/media/trailer/manifest.json";

Expand Down Expand Up @@ -125,6 +126,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
</nav>
</header>
<main id="main">{children}</main>
<AdUnit />
<footer className="footer">
<div className="footer__columns">
<div>
Expand Down Expand Up @@ -268,6 +270,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
src="https://crawlproof.com/stats.js"
strategy="afterInteractive"
/>
<Script src="https://crawlproof.com/ad.js" strategy="afterInteractive" />
</body>
</html>
);
Expand Down
Loading