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 (
+
+ );
+}
diff --git a/surfsense_web/public/ads.txt b/surfsense_web/public/ads.txt
new file mode 100644
index 000000000..ff648beb6
--- /dev/null
+++ b/surfsense_web/public/ads.txt
@@ -0,0 +1 @@
+google.com, pub-4479898201883964, DIRECT, f08c47fec0942fa0