diff --git a/apps/sim/app/(landing)/layout.tsx b/apps/sim/app/(landing)/layout.tsx index 740deccce9f..9fef6524507 100644 --- a/apps/sim/app/(landing)/layout.tsx +++ b/apps/sim/app/(landing)/layout.tsx @@ -6,9 +6,18 @@ import { isHosted } from '@/lib/core/config/env-flags' import { SITE_URL } from '@/lib/core/utils/urls' import { LandingShell } from '@/app/(landing)/components' import { HubspotPageViewTracker } from '@/app/(landing)/hubspot-page-view-tracker' +import { XPageViewTracker } from '@/app/(landing)/x-page-view-tracker' const HUBSPOT_SCRIPT_SRC = 'https://js-na2.hs-scripts.com/246720681.js' as const +const X_PIXEL_ID = 'q5xbl' as const + +/** X (Twitter) conversion tracking base code — loads uwt.js and fires the initial PageView. */ +const X_PIXEL_BASE_CODE = `!function(e,t,n,s,u,a){e.twq||(s=e.twq=function(){s.exe?s.exe.apply(s,arguments):s.queue.push(arguments); +},s.version='1.1',s.queue=[],u=t.createElement(n),u.async=!0,u.src='https://static.ads-twitter.com/uwt.js', +a=t.getElementsByTagName(n)[0],a.parentNode.insertBefore(u,a))}(window,document,'script'); +twq('config','${X_PIXEL_ID}');` + /** * Route-group layout for the entire landing family - the home page, platform and * solutions pages, pricing, legal, and the marketing subroutes (blog, models, @@ -33,12 +42,16 @@ export default function LandingLayout({ children }: { children: ReactNode }) { return ( {children} - {/* HubSpot tracking — hosted only */} + {/* HubSpot + X pixel tracking — hosted only */} {isHosted && ( <> + )} diff --git a/apps/sim/app/(landing)/x-page-view-tracker.tsx b/apps/sim/app/(landing)/x-page-view-tracker.tsx new file mode 100644 index 00000000000..5db1e0439fd --- /dev/null +++ b/apps/sim/app/(landing)/x-page-view-tracker.tsx @@ -0,0 +1,46 @@ +'use client' + +import { useEffect, useRef } from 'react' +import { usePathname, useSearchParams } from 'next/navigation' + +declare global { + interface Window { + twq?: (...args: unknown[]) => void + } +} + +// next/script dedupes by id and never reloads on remount, so this must be +// module-scope (not a ref) to survive LandingLayout unmounting/remounting. +let hasTrackedInitialPageView = false + +/** + * The X pixel base code only auto-tracks the first page load; LandingLayout + * persists across client-side navigations, so the pixel never sees the rest. + * Re-fires the pixel's PageView via `twq('config', ...)` on every navigation + * after the first. + */ +export function XPageViewTracker() { + const pathname = usePathname() + const searchParams = useSearchParams() + const query = searchParams.toString() + + // Instance-scoped (not module-scoped) so a Strict Mode replay of this + // mount's effect is skipped, while a fresh mount — returning to the landing + // layout from the app — starts empty and tracks the view again. + const lastTrackedUrlRef = useRef(null) + + useEffect(() => { + const url = query ? `${pathname}?${query}` : pathname + if (lastTrackedUrlRef.current === url) return + lastTrackedUrlRef.current = url + + if (!hasTrackedInitialPageView) { + hasTrackedInitialPageView = true + return + } + + window.twq?.('config', 'q5xbl') + }, [pathname, query]) + + return null +}