From 2eaf4fbce19ffc1abc3a620aa19453eb549215f6 Mon Sep 17 00:00:00 2001 From: "DESKTOP-RTLN3BA\\$punk" Date: Thu, 21 May 2026 21:01:10 -0700 Subject: [PATCH] feat: added adsense on /free page --- surfsense_web/.env.example | 11 ++- surfsense_web/app/(home)/free/page.tsx | 20 +++++ surfsense_web/components/ads/ad-unit.tsx | 78 +++++++++++++++++++ .../components/ads/adsense-config.ts | 13 ++++ .../components/ads/adsense-script.tsx | 27 +++++++ surfsense_web/public/ads.txt | 1 + 6 files changed, 149 insertions(+), 1 deletion(-) create mode 100644 surfsense_web/components/ads/ad-unit.tsx create mode 100644 surfsense_web/components/ads/adsense-config.ts create mode 100644 surfsense_web/components/ads/adsense-script.tsx create mode 100644 surfsense_web/public/ads.txt diff --git a/surfsense_web/.env.example b/surfsense_web/.env.example index b121daf0b..5fb9d07d1 100644 --- a/surfsense_web/.env.example +++ b/surfsense_web/.env.example @@ -18,4 +18,13 @@ NEXT_PUBLIC_POSTHOG_KEY= # Cloudflare Turnstile CAPTCHA for anonymous chat abuse prevention # Get your site key from https://dash.cloudflare.com/ -> Turnstile -NEXT_PUBLIC_TURNSTILE_SITE_KEY= \ No newline at end of file +NEXT_PUBLIC_TURNSTILE_SITE_KEY= + +# Google AdSense (optional, only enables ads on the /free hub page). +# Publisher ID from your AdSense dashboard, e.g. ca-pub-XXXXXXXXXXXXXXXX. +# Leave empty to disable ad rendering entirely. +NEXT_PUBLIC_GOOGLE_ADSENSE_CLIENT_ID= +# Ad unit slot IDs from AdSense dashboard -> Ads -> By ad unit. +# Leave empty to hide individual slots while keeping the script loaded. +NEXT_PUBLIC_GOOGLE_ADSENSE_SLOT_FREE_HUB_IN_CONTENT= +NEXT_PUBLIC_GOOGLE_ADSENSE_SLOT_FREE_HUB_BEFORE_FAQ= \ No newline at end of file diff --git a/surfsense_web/app/(home)/free/page.tsx b/surfsense_web/app/(home)/free/page.tsx index cd75e7908..4512f3396 100644 --- a/surfsense_web/app/(home)/free/page.tsx +++ b/surfsense_web/app/(home)/free/page.tsx @@ -1,6 +1,9 @@ import { SquareArrowOutUpRight } from "lucide-react"; import type { Metadata } from "next"; import Link from "next/link"; +import { AdUnit } from "@/components/ads/ad-unit"; +import { ADSENSE_SLOTS } from "@/components/ads/adsense-config"; +import { AdSenseScript } from "@/components/ads/adsense-script"; import { BreadcrumbNav } from "@/components/seo/breadcrumb-nav"; import { FAQJsonLd, JsonLd } from "@/components/seo/json-ld"; import { Badge } from "@/components/ui/badge"; @@ -157,6 +160,7 @@ export default async function FreeHubPage() { return (
+ + {/* In-content ad: above the model table */} + + {/* Model Table */} {seoModels.length > 0 ? (
+ {/* In-content ad: after CTA, before FAQ */} + + {/* FAQ */}

Frequently Asked Questions

diff --git a/surfsense_web/components/ads/ad-unit.tsx b/surfsense_web/components/ads/ad-unit.tsx new file mode 100644 index 000000000..5f5860607 --- /dev/null +++ b/surfsense_web/components/ads/ad-unit.tsx @@ -0,0 +1,78 @@ +"use client"; + +import type { CSSProperties } from "react"; +import { useEffect, useRef } from "react"; +import { cn } from "@/lib/utils"; + +const ADSENSE_CLIENT_ID = process.env.NEXT_PUBLIC_GOOGLE_ADSENSE_CLIENT_ID; + +declare global { + interface Window { + adsbygoogle?: Record[]; + } +} + +interface AdUnitProps { + /** AdSense ad slot ID from your AdSense dashboard. */ + slot: string; + /** AdSense ad format. Defaults to "auto" for responsive display ads. */ + format?: "auto" | "fluid" | "rectangle" | "vertical" | "horizontal"; + /** Optional layout (e.g. "in-article"). */ + layout?: string; + /** Optional layout key (required for in-feed ads). */ + layoutKey?: string; + /** Full-width responsive on mobile. Defaults to true. */ + responsive?: boolean; + className?: string; + style?: CSSProperties; +} + +/** + * Renders a Google AdSense ad unit. Requires to be mounted + * on the same page. Renders nothing if NEXT_PUBLIC_GOOGLE_ADSENSE_CLIENT_ID + * is unset or if `slot` is empty (so missing-slot env vars stay invisible). + */ +export function AdUnit({ + slot, + format = "auto", + layout, + layoutKey, + responsive = true, + className, + style, +}: AdUnitProps) { + const insRef = useRef(null); + + useEffect(() => { + if (!ADSENSE_CLIENT_ID || !slot) return; + const el = insRef.current; + if (!el) return; + // Guard against duplicate pushes (React StrictMode dev double-invoke, + // client-side navigation back to this page, or HMR remounts). AdSense + // sets data-adsbygoogle-status="done" once it has filled a slot. + if (el.getAttribute("data-adsbygoogle-status")) return; + try { + (window.adsbygoogle = window.adsbygoogle || []).push({}); + } catch { + // AdSense throws if pushed before the script has loaded or on + // duplicate pushes. The script processes pending pushes when it + // finishes loading, so we can safely swallow this. + } + }, [slot]); + + if (!ADSENSE_CLIENT_ID || !slot) return null; + + return ( + + ); +} diff --git a/surfsense_web/components/ads/adsense-config.ts b/surfsense_web/components/ads/adsense-config.ts new file mode 100644 index 000000000..f5d22908b --- /dev/null +++ b/surfsense_web/components/ads/adsense-config.ts @@ -0,0 +1,13 @@ +/** + * Centralized AdSense ad slot IDs. + * + * After creating ad units in your AdSense dashboard (Ads → By ad unit), paste + * the numeric slot IDs into the corresponding env vars below. Empty slot IDs + * render nothing (see ), so partial rollout is safe. + */ +export const ADSENSE_SLOTS = { + /** /free hub: between the model table and "Why SurfSense" section. */ + freeHubInContent: process.env.NEXT_PUBLIC_GOOGLE_ADSENSE_SLOT_FREE_HUB_IN_CONTENT ?? "", + /** /free hub: between the CTA and the FAQ section. */ + freeHubBeforeFaq: process.env.NEXT_PUBLIC_GOOGLE_ADSENSE_SLOT_FREE_HUB_BEFORE_FAQ ?? "", +} as const; diff --git a/surfsense_web/components/ads/adsense-script.tsx b/surfsense_web/components/ads/adsense-script.tsx new file mode 100644 index 000000000..e2636b333 --- /dev/null +++ b/surfsense_web/components/ads/adsense-script.tsx @@ -0,0 +1,27 @@ +"use client"; + +import Script from "next/script"; + +const ADSENSE_CLIENT_ID = process.env.NEXT_PUBLIC_GOOGLE_ADSENSE_CLIENT_ID; + +/** + * Loads the Google AdSense library (adsbygoogle.js). Mount this once on any + * route that renders instances. Scoped per-route (not in the root + * layout) so the third-party script is not shipped on unrelated pages. + * + * Renders nothing if NEXT_PUBLIC_GOOGLE_ADSENSE_CLIENT_ID is unset, so dev and + * preview deployments without the env var stay ad-free. + */ +export function AdSenseScript() { + if (!ADSENSE_CLIENT_ID) return null; + + return ( +