mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-25 19:15:18 +02:00
feat: added adsense on /free page
This commit is contained in:
parent
2e589091d8
commit
2eaf4fbce1
6 changed files with 149 additions and 1 deletions
|
|
@ -18,4 +18,13 @@ NEXT_PUBLIC_POSTHOG_KEY=
|
||||||
|
|
||||||
# Cloudflare Turnstile CAPTCHA for anonymous chat abuse prevention
|
# Cloudflare Turnstile CAPTCHA for anonymous chat abuse prevention
|
||||||
# Get your site key from https://dash.cloudflare.com/ -> Turnstile
|
# Get your site key from https://dash.cloudflare.com/ -> Turnstile
|
||||||
NEXT_PUBLIC_TURNSTILE_SITE_KEY=
|
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=
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
import { SquareArrowOutUpRight } from "lucide-react";
|
import { SquareArrowOutUpRight } from "lucide-react";
|
||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import Link from "next/link";
|
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 { BreadcrumbNav } from "@/components/seo/breadcrumb-nav";
|
||||||
import { FAQJsonLd, JsonLd } from "@/components/seo/json-ld";
|
import { FAQJsonLd, JsonLd } from "@/components/seo/json-ld";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
|
@ -157,6 +160,7 @@ export default async function FreeHubPage() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen pt-20">
|
<div className="min-h-screen pt-20">
|
||||||
|
<AdSenseScript />
|
||||||
<JsonLd
|
<JsonLd
|
||||||
data={{
|
data={{
|
||||||
"@context": "https://schema.org",
|
"@context": "https://schema.org",
|
||||||
|
|
@ -216,6 +220,14 @@ export default async function FreeHubPage() {
|
||||||
|
|
||||||
<Separator className="my-12 max-w-4xl mx-auto" />
|
<Separator className="my-12 max-w-4xl mx-auto" />
|
||||||
|
|
||||||
|
{/* In-content ad: above the model table */}
|
||||||
|
<aside
|
||||||
|
aria-label="Advertisement"
|
||||||
|
className="max-w-4xl mx-auto mb-8 min-h-[100px]"
|
||||||
|
>
|
||||||
|
<AdUnit slot={ADSENSE_SLOTS.freeHubInContent} />
|
||||||
|
</aside>
|
||||||
|
|
||||||
{/* Model Table */}
|
{/* Model Table */}
|
||||||
{seoModels.length > 0 ? (
|
{seoModels.length > 0 ? (
|
||||||
<section
|
<section
|
||||||
|
|
@ -340,6 +352,14 @@ export default async function FreeHubPage() {
|
||||||
|
|
||||||
<Separator className="my-12 max-w-4xl mx-auto" />
|
<Separator className="my-12 max-w-4xl mx-auto" />
|
||||||
|
|
||||||
|
{/* In-content ad: after CTA, before FAQ */}
|
||||||
|
<aside
|
||||||
|
aria-label="Advertisement"
|
||||||
|
className="max-w-3xl mx-auto my-8 min-h-[100px]"
|
||||||
|
>
|
||||||
|
<AdUnit slot={ADSENSE_SLOTS.freeHubBeforeFaq} />
|
||||||
|
</aside>
|
||||||
|
|
||||||
{/* FAQ */}
|
{/* FAQ */}
|
||||||
<section className="max-w-3xl mx-auto">
|
<section className="max-w-3xl mx-auto">
|
||||||
<h2 className="text-2xl font-bold text-center mb-8">Frequently Asked Questions</h2>
|
<h2 className="text-2xl font-bold text-center mb-8">Frequently Asked Questions</h2>
|
||||||
|
|
|
||||||
78
surfsense_web/components/ads/ad-unit.tsx
Normal file
78
surfsense_web/components/ads/ad-unit.tsx
Normal file
|
|
@ -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<string, unknown>[];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 <AdSenseScript /> 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<HTMLModElement>(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 (
|
||||||
|
<ins
|
||||||
|
ref={insRef}
|
||||||
|
className={cn("adsbygoogle block", className)}
|
||||||
|
style={{ display: "block", ...style }}
|
||||||
|
data-ad-client={ADSENSE_CLIENT_ID}
|
||||||
|
data-ad-slot={slot}
|
||||||
|
data-ad-format={format}
|
||||||
|
data-ad-layout={layout}
|
||||||
|
data-ad-layout-key={layoutKey}
|
||||||
|
data-full-width-responsive={responsive ? "true" : "false"}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
13
surfsense_web/components/ads/adsense-config.ts
Normal file
13
surfsense_web/components/ads/adsense-config.ts
Normal file
|
|
@ -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 <AdUnit />), 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;
|
||||||
27
surfsense_web/components/ads/adsense-script.tsx
Normal file
27
surfsense_web/components/ads/adsense-script.tsx
Normal file
|
|
@ -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 <AdUnit /> 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 (
|
||||||
|
<Script
|
||||||
|
id="adsbygoogle-init"
|
||||||
|
async
|
||||||
|
strategy="afterInteractive"
|
||||||
|
src={`https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=${ADSENSE_CLIENT_ID}`}
|
||||||
|
crossOrigin="anonymous"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
1
surfsense_web/public/ads.txt
Normal file
1
surfsense_web/public/ads.txt
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
google.com, pub-4479898201883964, DIRECT, f08c47fec0942fa0
|
||||||
Loading…
Add table
Add a link
Reference in a new issue