Add UTM parameter preservation for Copilot plans page (#57674)
This commit is contained in:
106
src/frame/components/UtmPreserver.tsx
Normal file
106
src/frame/components/UtmPreserver.tsx
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
import { useEffect } from 'react'
|
||||||
|
import { useRouter } from 'next/router'
|
||||||
|
|
||||||
|
type UtmPreserverProps = {
|
||||||
|
// CSS selector for links that should preserve UTM parameters
|
||||||
|
linkSelector?: string
|
||||||
|
// Specific page paths where this component should be active
|
||||||
|
activePaths?: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const UtmPreserver = ({
|
||||||
|
linkSelector = 'a[href*="github.com/copilot"], a[href*="github.com/github-copilot"]',
|
||||||
|
activePaths = ['/copilot/get-started/plans'],
|
||||||
|
}: UtmPreserverProps) => {
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Check if current page should have UTM preservation
|
||||||
|
const shouldPreserveUtm = activePaths.some((path) => router.asPath.includes(path))
|
||||||
|
if (!shouldPreserveUtm) return
|
||||||
|
|
||||||
|
// Extract UTM parameters from current URL
|
||||||
|
const getUtmParams = (): URLSearchParams => {
|
||||||
|
const urlParams = new URLSearchParams(window.location.search)
|
||||||
|
const utmParams = new URLSearchParams()
|
||||||
|
|
||||||
|
for (const [key, value] of urlParams) {
|
||||||
|
if (key.startsWith('utm_')) {
|
||||||
|
utmParams.set(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return utmParams
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add UTM parameters to a URL
|
||||||
|
const addUtmParamsToUrl = (url: string, utmParams: URLSearchParams): string => {
|
||||||
|
try {
|
||||||
|
const urlObj = new URL(url)
|
||||||
|
|
||||||
|
for (const [key, value] of utmParams) {
|
||||||
|
urlObj.searchParams.set(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return urlObj.toString()
|
||||||
|
} catch {
|
||||||
|
// If URL parsing fails, return original URL
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply UTM parameters to relevant links
|
||||||
|
const applyUtmToLinks = (): void => {
|
||||||
|
const utmParams = getUtmParams()
|
||||||
|
|
||||||
|
if (utmParams.toString() === '') return
|
||||||
|
|
||||||
|
const links = document.querySelectorAll<HTMLAnchorElement>(linkSelector)
|
||||||
|
|
||||||
|
links.forEach((link) => {
|
||||||
|
if (link.href && (link.href.startsWith('http://') || link.href.startsWith('https://'))) {
|
||||||
|
link.href = addUtmParamsToUrl(link.href, utmParams)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle click events for dynamic link modification
|
||||||
|
const handleLinkClick = (event: Event): void => {
|
||||||
|
const link = (event.target as Element)?.closest('a') as HTMLAnchorElement
|
||||||
|
if (!link) return
|
||||||
|
|
||||||
|
// Check if this link matches our selector
|
||||||
|
if (!link.matches(linkSelector)) return
|
||||||
|
|
||||||
|
const utmParams = getUtmParams()
|
||||||
|
if (utmParams.toString() === '') return
|
||||||
|
|
||||||
|
if (link.href && (link.href.startsWith('http://') || link.href.startsWith('https://'))) {
|
||||||
|
link.href = addUtmParamsToUrl(link.href, utmParams)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply UTM parameters immediately to existing links
|
||||||
|
applyUtmToLinks()
|
||||||
|
|
||||||
|
// Also handle clicks for any dynamically added links
|
||||||
|
document.addEventListener('click', handleLinkClick, true)
|
||||||
|
|
||||||
|
// Re-apply when the route changes (for single-page navigation)
|
||||||
|
const handleRouteChange = () => {
|
||||||
|
// Small delay to ensure DOM has updated
|
||||||
|
setTimeout(applyUtmToLinks, 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
router.events.on('routeChangeComplete', handleRouteChange)
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('click', handleLinkClick, true)
|
||||||
|
router.events.off('routeChangeComplete', handleRouteChange)
|
||||||
|
}
|
||||||
|
}, [router.asPath, router.events, linkSelector, activePaths])
|
||||||
|
|
||||||
|
// This component doesn't render anything
|
||||||
|
return null
|
||||||
|
}
|
||||||
@@ -21,6 +21,7 @@ import { Breadcrumbs } from '@/frame/components/page-header/Breadcrumbs'
|
|||||||
import { Link } from '@/frame/components/Link'
|
import { Link } from '@/frame/components/Link'
|
||||||
import { useTranslation } from '@/languages/components/useTranslation'
|
import { useTranslation } from '@/languages/components/useTranslation'
|
||||||
import { LinkPreviewPopover } from '@/links/components/LinkPreviewPopover'
|
import { LinkPreviewPopover } from '@/links/components/LinkPreviewPopover'
|
||||||
|
import { UtmPreserver } from '@/frame/components/UtmPreserver'
|
||||||
|
|
||||||
const ClientSideRefresh = dynamic(() => import('@/frame/components/ClientSideRefresh'), {
|
const ClientSideRefresh = dynamic(() => import('@/frame/components/ClientSideRefresh'), {
|
||||||
ssr: false,
|
ssr: false,
|
||||||
@@ -101,6 +102,7 @@ export const ArticlePage = () => {
|
|||||||
return (
|
return (
|
||||||
<DefaultLayout>
|
<DefaultLayout>
|
||||||
<LinkPreviewPopover />
|
<LinkPreviewPopover />
|
||||||
|
<UtmPreserver />
|
||||||
{isDev && <ClientSideRefresh />}
|
{isDev && <ClientSideRefresh />}
|
||||||
{router.pathname.includes('/rest/') && <RestRedirect />}
|
{router.pathname.includes('/rest/') && <RestRedirect />}
|
||||||
{currentLayout === 'inline' ? (
|
{currentLayout === 'inline' ? (
|
||||||
|
|||||||
Reference in New Issue
Block a user