Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion apps/sim/app/(landing)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -33,12 +42,16 @@ export default function LandingLayout({ children }: { children: ReactNode }) {
return (
<LandingShell>
{children}
{/* HubSpot tracking — hosted only */}
{/* HubSpot + X pixel tracking — hosted only */}
{isHosted && (
<>
<Script id='hs-script-loader' src={HUBSPOT_SCRIPT_SRC} strategy='afterInteractive' />
<Script id='x-pixel-base' strategy='afterInteractive'>
{X_PIXEL_BASE_CODE}
</Script>
<Suspense fallback={null}>
<HubspotPageViewTracker />
<XPageViewTracker />
</Suspense>
</>
)}
Expand Down
46 changes: 46 additions & 0 deletions apps/sim/app/(landing)/x-page-view-tracker.tsx
Original file line number Diff line number Diff line change
@@ -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<string | null>(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
}
Loading