1
0
mirror of synced 2025-12-23 21:07:12 -05:00

Expand UtmPreserver to all external github.com links on article and landing pages (#57920)

This commit is contained in:
Kevin Heis
2025-10-14 09:51:36 -07:00
committed by GitHub
parent ed5b9ca3ad
commit e4cf0d2dd4
8 changed files with 36 additions and 30 deletions

View File

@@ -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

View File

@@ -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. */}

View File

@@ -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>

View File

@@ -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">

View File

@@ -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. */}

View File

@@ -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} />

View File

@@ -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">

View File

@@ -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} />