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
5 changes: 5 additions & 0 deletions src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -128,3 +128,8 @@ dl.facts dd { margin: 0; overflow-wrap: anywhere; }
.site-footer nav { display: flex; gap: 1rem; flex-wrap: wrap; margin-bottom: 0.75rem; }
.site-footer a { color: var(--muted); }
.site-footer + div > .pfs-footer { border-top: 0; }

/* CrawlProof ad unit: no box until ad.js starts filling it, so an unsold or blocked slot leaves no hole. */
.ad-unit { display: flex; flex-direction: column; align-items: center; height: 0; overflow: hidden; }
.ad-unit[data-cp-filled] { height: auto; min-height: 106px; margin: 1.5rem 0; overflow: visible; }
.ad-unit[data-cp-filled][data-format="banner_300x250"] { min-height: 266px; }
5 changes: 5 additions & 0 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Metadata, Viewport } from "next";
import { Footer } from "@profullstack/footer/react";
import Link from "next/link";
import { AdUnit } from "@/components/ad-unit";
import { RegisterServiceWorker } from "@/components/register-sw";
import { DISCLOSURE } from "@/lib/site";
import "./globals.css";
Expand Down Expand Up @@ -46,6 +47,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
<head>
{/* CrawlProof stats: a plain async tag so it is in the served HTML (next/script afterInteractive is not). */}
<script data-site="6be9abdb-fd93-4756-bbf4-3ebefc3117c3" src="https://crawlproof.com/stats.js" async></script>
<script src="https://crawlproof.com/ad.js" async></script>
</head>
<body>
<a className="skip" href="#main">Skip to content</a>
Expand All @@ -63,6 +65,9 @@ export default function RootLayout({ children }: { children: React.ReactNode })
</div>
</header>
<main id="main" className="wrap">{children}</main>
<div className="wrap">
<AdUnit />
</div>
<div className="site-footer">
<div className="wrap">
<p>{DISCLOSURE}</p>
Expand Down
44 changes: 44 additions & 0 deletions src/components/ad-unit.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
"use client";

import { useEffect, useRef } from "react";

/** CrawlProof ad slot for datamart.help. Manage it at https://crawlproof.com/ads/slots */
const AD_SLOT = "04392e64-733a-48dd-b17b-c4dc9eb3a399";

/** 728px banner plus the .wrap gutters; anything narrower gets the 300x250 rectangle. */
const LEADERBOARD_QUERY = "(min-width: 760px)";

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

/**
* One CrawlProof unit above the footer. The aside is server-rendered without
* data-slot, so ad.js's own DOMContentLoaded pass skips it; the format is picked
* from the real viewport on mount, then we ask ad.js to scan (retrying briefly
* until it has loaded, and giving up so an ad blocker leaves no timer running).
* It stays zero-height until ad.js marks it data-cp-filled.
*/
export function AdUnit() {
const ref = useRef<HTMLElement>(null);

useEffect(() => {
const el = ref.current;
if (!el) return;
el.dataset.format = window.matchMedia(LEADERBOARD_QUERY).matches ? "banner_728x90" : "banner_300x250";
el.dataset.slot = AD_SLOT;

let tries = 0;
let timer: ReturnType<typeof setTimeout> | undefined;
const tick = () => {
if (window.crawlproofAds) return window.crawlproofAds.scan();
if (++tries <= 25) timer = setTimeout(tick, 200);
};
tick();
return () => clearTimeout(timer);
}, []);

return <aside ref={ref} className="ad-unit" data-cp-ad="" aria-label="Advertisement" />;
}
Loading