Expand UtmPreserver to all external github.com links on article and landing pages (#57920)
This commit is contained in:
@@ -1,24 +1,10 @@
|
||||
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) => {
|
||||
export const UtmPreserver = () => {
|
||||
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)
|
||||
@@ -33,6 +19,22 @@ export const UtmPreserver = ({
|
||||
return utmParams
|
||||
}
|
||||
|
||||
const utmParams = getUtmParams()
|
||||
if (utmParams.toString() === '') return
|
||||
|
||||
// Check if a link should have UTM parameters preserved
|
||||
const shouldPreserveUtm = (url: string): boolean => {
|
||||
const lowercaseUrl = url.toLowerCase()
|
||||
|
||||
// Preserve UTM for any external github.com links (including subdomains like blog.github.com)
|
||||
// but NOT for docs.github.com (which are internal links anyway)
|
||||
const hasProtocol = lowercaseUrl.startsWith('https://') || lowercaseUrl.startsWith('http://')
|
||||
const isGithubCom = lowercaseUrl.includes('github.com')
|
||||
const isDocsGithubCom = lowercaseUrl.includes('docs.github.com')
|
||||
|
||||
return hasProtocol && isGithubCom && !isDocsGithubCom
|
||||
}
|
||||
|
||||
// Add UTM parameters to a URL
|
||||
const addUtmParamsToUrl = (url: string, utmParams: URLSearchParams): string => {
|
||||
try {
|
||||
@@ -51,14 +53,10 @@ export const UtmPreserver = ({
|
||||
|
||||
// Apply UTM parameters to relevant links
|
||||
const applyUtmToLinks = (): void => {
|
||||
const utmParams = getUtmParams()
|
||||
|
||||
if (utmParams.toString() === '') return
|
||||
|
||||
const links = document.querySelectorAll<HTMLAnchorElement>(linkSelector)
|
||||
const links = document.querySelectorAll<HTMLAnchorElement>('a[href]')
|
||||
|
||||
links.forEach((link) => {
|
||||
if (link.href && (link.href.startsWith('http://') || link.href.startsWith('https://'))) {
|
||||
if (link.href && shouldPreserveUtm(link.href)) {
|
||||
link.href = addUtmParamsToUrl(link.href, utmParams)
|
||||
}
|
||||
})
|
||||
@@ -67,15 +65,9 @@ export const UtmPreserver = ({
|
||||
// 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
|
||||
if (!link || !link.href) 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://'))) {
|
||||
if (shouldPreserveUtm(link.href)) {
|
||||
link.href = addUtmParamsToUrl(link.href, utmParams)
|
||||
}
|
||||
}
|
||||
@@ -99,7 +91,7 @@ export const UtmPreserver = ({
|
||||
document.removeEventListener('click', handleLinkClick, true)
|
||||
router.events.off('routeChangeComplete', handleRouteChange)
|
||||
}
|
||||
}, [router.asPath, router.events, linkSelector, activePaths])
|
||||
}, [router.asPath, router.events])
|
||||
|
||||
// This component doesn't render anything
|
||||
return null
|
||||
|
||||
@@ -12,6 +12,7 @@ import { ClientSideRedirects } from '@/rest/components/ClientSideRedirects'
|
||||
import { RestRedirect } from '@/rest/components/RestRedirect'
|
||||
import { Breadcrumbs } from '@/frame/components/page-header/Breadcrumbs'
|
||||
import { ArticleCardItems } from '@/landings/types'
|
||||
import { UtmPreserver } from '@/frame/components/UtmPreserver'
|
||||
|
||||
export const CategoryLanding = () => {
|
||||
const { t } = useTranslation('cookbook_landing')
|
||||
@@ -97,6 +98,7 @@ export const CategoryLanding = () => {
|
||||
|
||||
return (
|
||||
<DefaultLayout>
|
||||
<UtmPreserver />
|
||||
{router.route === '/[versionId]/rest/[category]' && <RestRedirect />}
|
||||
{/* Doesn't matter *where* this is included because it will
|
||||
never render anything. It always just return null. */}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { LearningTracks } from '@/learning-track/components/guides/LearningTrack
|
||||
import { ArticleCards } from '@/landings/components/ArticleCards'
|
||||
import { useTranslation } from '@/languages/components/useTranslation'
|
||||
import { useMainContext } from '@/frame/components/context/MainContext'
|
||||
import { UtmPreserver } from '@/frame/components/UtmPreserver'
|
||||
|
||||
export const ProductGuides = () => {
|
||||
const { title, learningTracks, includeGuides } = useProductGuidesContext()
|
||||
@@ -18,6 +19,7 @@ export const ProductGuides = () => {
|
||||
|
||||
return (
|
||||
<DefaultLayout>
|
||||
<UtmPreserver />
|
||||
<LandingSection className="pt-3">
|
||||
<GuidesHero />
|
||||
</LandingSection>
|
||||
|
||||
@@ -13,6 +13,7 @@ import { ProductArticlesList } from '@/landings/components/ProductArticlesList'
|
||||
import { ProductReleases } from '@/landings/components/ProductReleases'
|
||||
import { useVersion } from '@/versions/components/useVersion'
|
||||
import { RestRedirect } from '@/rest/components/RestRedirect'
|
||||
import { UtmPreserver } from '@/frame/components/UtmPreserver'
|
||||
|
||||
export const ProductLanding = () => {
|
||||
const router = useRouter()
|
||||
@@ -23,6 +24,7 @@ export const ProductLanding = () => {
|
||||
|
||||
return (
|
||||
<DefaultLayout>
|
||||
<UtmPreserver />
|
||||
<div data-search="article-body">
|
||||
{router.query.productId === 'rest' && <RestRedirect />}
|
||||
<LandingSection className="pt-3">
|
||||
|
||||
@@ -15,6 +15,7 @@ import { LearningTrackNav } from '@/learning-track/components/article/LearningTr
|
||||
import { ClientSideRedirects } from '@/rest/components/ClientSideRedirects'
|
||||
import { RestRedirect } from '@/rest/components/RestRedirect'
|
||||
import { Breadcrumbs } from '@/frame/components/page-header/Breadcrumbs'
|
||||
import { UtmPreserver } from '@/frame/components/UtmPreserver'
|
||||
|
||||
export const TocLanding = () => {
|
||||
const router = useRouter()
|
||||
@@ -33,6 +34,7 @@ export const TocLanding = () => {
|
||||
|
||||
return (
|
||||
<DefaultLayout>
|
||||
<UtmPreserver />
|
||||
{router.route === '/[versionId]/rest/[category]' && <RestRedirect />}
|
||||
{/* Doesn't matter *where* this is included because it will
|
||||
never render anything. It always just return null. */}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { DefaultLayout } from '@/frame/components/DefaultLayout'
|
||||
import { useLandingContext } from '@/landings/context/LandingContext'
|
||||
import { LandingHero } from '@/landings/components/shared/LandingHero'
|
||||
import { ArticleGrid } from '@/landings/components/shared/LandingArticleGridWithFilter'
|
||||
import { UtmPreserver } from '@/frame/components/UtmPreserver'
|
||||
import { LandingCarousel } from '@/landings/components/shared/LandingCarousel'
|
||||
|
||||
import type { ArticleCardItems } from '@/landings/types'
|
||||
@@ -18,6 +19,7 @@ export const BespokeLanding = () => {
|
||||
|
||||
return (
|
||||
<DefaultLayout>
|
||||
<UtmPreserver />
|
||||
<div data-search="article-body">
|
||||
<LandingHero title={title} intro={intro} heroImage={heroImage} introLinks={introLinks} />
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useLandingContext } from '@/landings/context/LandingContext'
|
||||
import { LandingHero } from '@/landings/components/shared/LandingHero'
|
||||
import { ArticleGrid } from '@/landings/components/shared/LandingArticleGridWithFilter'
|
||||
import { LandingCarousel } from '@/landings/components/shared/LandingCarousel'
|
||||
import { UtmPreserver } from '@/frame/components/UtmPreserver'
|
||||
|
||||
import type { ArticleCardItems } from '@/landings/types'
|
||||
|
||||
@@ -18,6 +19,7 @@ export const DiscoveryLanding = () => {
|
||||
|
||||
return (
|
||||
<DefaultLayout>
|
||||
<UtmPreserver />
|
||||
<div>
|
||||
<LandingHero title={title} intro={intro} heroImage={heroImage} introLinks={introLinks} />
|
||||
<div className="container-xl px-3 px-md-6 mt-6 mb-4">
|
||||
|
||||
@@ -2,12 +2,14 @@ import { DefaultLayout } from '@/frame/components/DefaultLayout'
|
||||
import { useLandingContext } from '@/landings/context/LandingContext'
|
||||
import { LandingHero } from '@/landings/components/shared/LandingHero'
|
||||
import { JourneyLearningTracks } from './JourneyLearningTracks'
|
||||
import { UtmPreserver } from '@/frame/components/UtmPreserver'
|
||||
|
||||
export const JourneyLanding = () => {
|
||||
const { title, intro, heroImage, introLinks, journeyTracks } = useLandingContext()
|
||||
|
||||
return (
|
||||
<DefaultLayout>
|
||||
<UtmPreserver />
|
||||
<div>
|
||||
<LandingHero title={title} intro={intro} heroImage={heroImage} introLinks={introLinks} />
|
||||
|
||||
|
||||
Reference in New Issue
Block a user