diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 17b6b45..ea3a66a 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -9,6 +9,7 @@ import { Toaster } from "sonner"; import { SubscribeModal } from "@/components/subscribe"; import { IdentifyUser } from "@/components/auth/IdentifyUser"; import { HubspotTracking } from "@/components/HubspotTracking"; +import { XPixel } from "@/components/XPixel"; const GA_ID = process.env.NEXT_PUBLIC_GA_MEASUREMENT_ID; @@ -77,6 +78,7 @@ export default function RootLayout({ + ); diff --git a/src/components/XPixel.tsx b/src/components/XPixel.tsx new file mode 100644 index 0000000..9391f5f --- /dev/null +++ b/src/components/XPixel.tsx @@ -0,0 +1,69 @@ +"use client"; + +import Script from "next/script"; +import { usePathname, useSearchParams } from "next/navigation"; +import { Suspense, useEffect, useRef } from "react"; + +/* X (Twitter) Ads universal website tag. `twq('config', )` loads the + tag and reports the initial page load; conversion events are then reported + with twq('event', 'tw--', {...}) from wherever they happen. + + uwt.js installs no history listener, so App Router navigations would + otherwise go unreported. Each route change re-calls twq('config', ...), + which sends a fresh page-load beacon every time it runs (the command is + safe to repeat, and queues before the script finishes loading). + + Production only, so localhost and preview deployments stay out of the ad + data. To check the tag locally, temporarily drop the NODE_ENV guard. */ + +const X_PIXEL_ID = "re2t6"; + +declare global { + interface Window { + twq?: (...args: unknown[]) => void; + } +} + +function PageViewTracker() { + const pathname = usePathname(); + const searchParams = useSearchParams(); + // last path reported; doubles as the initial-load marker so the first + // render (already tracked by the inline snippet) and strict-mode effect + // re-runs don't double-count + const lastPath = useRef(null); + + useEffect(() => { + const query = searchParams?.toString(); + const path = query ? `${pathname}?${query}` : pathname; + + if (lastPath.current === null) { + lastPath.current = path; + return; + } + if (lastPath.current === path) return; + lastPath.current = path; + + window.twq?.("config", X_PIXEL_ID); + }, [pathname, searchParams]); + + return null; +} + +export function XPixel() { + if (process.env.NODE_ENV !== "production") return null; + + return ( + <> + + {/* useSearchParams requires a Suspense boundary in the App Router */} + + + + + ); +}