Browse Source

remove trackers when previewing, publishing, or rendering

imwald
Silberengel 5 months ago
parent
commit
01f8685d91
  1. 9
      src/components/Embedded/EmbeddedNormalUrl.tsx
  2. 7
      src/components/PostEditor/HighlightEditor.tsx
  3. 25
      src/components/PostEditor/PostContent.tsx
  4. 16
      src/components/PostEditor/PostTextarea/Preview.tsx
  5. 75
      src/lib/url.ts

9
src/components/Embedded/EmbeddedNormalUrl.tsx

@ -1,13 +1,18 @@
import { cleanUrl } from '@/lib/url'
export function EmbeddedNormalUrl({ url }: { url: string }) { export function EmbeddedNormalUrl({ url }: { url: string }) {
// Clean tracking parameters from URLs before displaying/linking
const cleanedUrl = cleanUrl(url)
return ( return (
<a <a
className="text-primary hover:underline" className="text-primary hover:underline"
href={url} href={cleanedUrl}
target="_blank" target="_blank"
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
rel="noreferrer" rel="noreferrer"
> >
{url} {cleanedUrl}
</a> </a>
) )
} }

7
src/components/PostEditor/HighlightEditor.tsx

@ -2,6 +2,7 @@ import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label' import { Label } from '@/components/ui/label'
import { Textarea } from '@/components/ui/textarea' import { Textarea } from '@/components/ui/textarea'
import { cleanUrl } from '@/lib/url'
import { X } from 'lucide-react' import { X } from 'lucide-react'
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
@ -38,11 +39,13 @@ export default function HighlightEditor({
} }
// Check if it's a URL // Check if it's a URL
if (sourceInput.startsWith('https://')) { if (sourceInput.startsWith('https://') || sourceInput.startsWith('http://')) {
// Clean tracking parameters from the URL before publishing
const cleanedUrl = cleanUrl(sourceInput)
setError('') setError('')
setHighlightData({ setHighlightData({
sourceType: 'url', sourceType: 'url',
sourceValue: sourceInput, sourceValue: cleanedUrl,
context context
}) })
return return

25
src/components/PostEditor/PostContent.tsx

@ -15,7 +15,7 @@ import { isTouchDevice } from '@/lib/utils'
import { useNostr } from '@/providers/NostrProvider' import { useNostr } from '@/providers/NostrProvider'
import { useFeed } from '@/providers/FeedProvider' import { useFeed } from '@/providers/FeedProvider'
import { useReply } from '@/providers/ReplyProvider' import { useReply } from '@/providers/ReplyProvider'
import { normalizeUrl } from '@/lib/url' import { normalizeUrl, cleanUrl } from '@/lib/url'
import postEditorCache from '@/services/post-editor-cache.service' import postEditorCache from '@/services/post-editor-cache.service'
import { TPollCreateData } from '@/types' import { TPollCreateData } from '@/types'
import { ImageUp, ListTodo, LoaderCircle, MessageCircle, Settings, Smile, X, Highlighter } from 'lucide-react' import { ImageUp, ListTodo, LoaderCircle, MessageCircle, Settings, Smile, X, Highlighter } from 'lucide-react'
@ -190,12 +190,23 @@ export default function PostContent({
let newEvent: any = null let newEvent: any = null
try { try {
// Clean tracking parameters from URLs in the post content
const cleanedText = text.replace(
/(https?:\/\/[^\s]+)/g,
(url) => {
try {
return cleanUrl(url)
} catch {
return url
}
}
)
if (isHighlight) { if (isHighlight) {
// For highlights, pass the original sourceValue which contains the full identifier // For highlights, pass the original sourceValue which contains the full identifier
// The createHighlightDraftEvent function will parse it correctly // The createHighlightDraftEvent function will parse it correctly
draftEvent = await createHighlightDraftEvent( draftEvent = await createHighlightDraftEvent(
text, cleanedText,
highlightData.sourceType, highlightData.sourceType,
highlightData.sourceValue, highlightData.sourceValue,
highlightData.context, highlightData.context,
@ -206,28 +217,28 @@ export default function PostContent({
} }
) )
} else if (isPublicMessage) { } else if (isPublicMessage) {
draftEvent = await createPublicMessageDraftEvent(text, extractedMentions, { draftEvent = await createPublicMessageDraftEvent(cleanedText, extractedMentions, {
addClientTag, addClientTag,
isNsfw isNsfw
}) })
} else if (parentEvent && parentEvent.kind === ExtendedKind.PUBLIC_MESSAGE) { } else if (parentEvent && parentEvent.kind === ExtendedKind.PUBLIC_MESSAGE) {
draftEvent = await createPublicMessageReplyDraftEvent(text, parentEvent, mentions, { draftEvent = await createPublicMessageReplyDraftEvent(cleanedText, parentEvent, mentions, {
addClientTag, addClientTag,
isNsfw isNsfw
}) })
} else if (parentEvent && parentEvent.kind !== kinds.ShortTextNote) { } else if (parentEvent && parentEvent.kind !== kinds.ShortTextNote) {
draftEvent = await createCommentDraftEvent(text, parentEvent, mentions, { draftEvent = await createCommentDraftEvent(cleanedText, parentEvent, mentions, {
addClientTag, addClientTag,
protectedEvent: isProtectedEvent, protectedEvent: isProtectedEvent,
isNsfw isNsfw
}) })
} else if (isPoll) { } else if (isPoll) {
draftEvent = await createPollDraftEvent(pubkey!, text, mentions, pollCreateData, { draftEvent = await createPollDraftEvent(pubkey!, cleanedText, mentions, pollCreateData, {
addClientTag, addClientTag,
isNsfw isNsfw
}) })
} else { } else {
draftEvent = await createShortTextNoteDraftEvent(text, mentions, { draftEvent = await createShortTextNoteDraftEvent(cleanedText, mentions, {
parentEvent, parentEvent,
addClientTag, addClientTag,
protectedEvent: isProtectedEvent, protectedEvent: isProtectedEvent,

16
src/components/PostEditor/PostTextarea/Preview.tsx

@ -1,6 +1,7 @@
import { Card } from '@/components/ui/card' import { Card } from '@/components/ui/card'
import { transformCustomEmojisInContent } from '@/lib/draft-event' import { transformCustomEmojisInContent } from '@/lib/draft-event'
import { createFakeEvent } from '@/lib/event' import { createFakeEvent } from '@/lib/event'
import { cleanUrl } from '@/lib/url'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { useMemo } from 'react' import { useMemo } from 'react'
import Content from '../../Content' import Content from '../../Content'
@ -15,7 +16,20 @@ export default function Preview({
kind?: number kind?: number
}) { }) {
const { content: processedContent, emojiTags } = useMemo( const { content: processedContent, emojiTags } = useMemo(
() => transformCustomEmojisInContent(content), () => {
// Clean tracking parameters from URLs in the preview
const cleanedContent = content.replace(
/(https?:\/\/[^\s]+)/g,
(url) => {
try {
return cleanUrl(url)
} catch {
return url
}
}
)
return transformCustomEmojisInContent(cleanedContent)
},
[content] [content]
) )
return ( return (

75
src/lib/url.ts

@ -144,3 +144,78 @@ export function isMedia(url: string) {
return false return false
} }
} }
/**
* Remove tracking parameters from URLs
* Removes common tracking parameters like utm_*, fbclid, gclid, etc.
*/
export function cleanUrl(url: string): string {
try {
const parsedUrl = new URL(url)
// List of tracking parameter prefixes and exact names to remove
const trackingParams = [
// Google Analytics & Ads
'utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',
'utm_id', 'utm_source_platform', 'utm_creative_format', 'utm_marketing_tactic',
'gclid', 'gclsrc', 'dclid', 'gbraid', 'wbraid',
// Facebook
'fbclid', 'fb_action_ids', 'fb_action_types', 'fb_source', 'fb_ref',
// Twitter/X
'twclid', 'twsrc',
// Microsoft/Bing
'msclkid', 'mc_cid', 'mc_eid',
// Adobe
'adobe_mc', 'adobe_mc_ref', 'adobe_mc_sdid',
// Mailchimp
'mc_cid', 'mc_eid',
// HubSpot
'hsCtaTracking', 'hsa_acc', 'hsa_cam', 'hsa_grp', 'hsa_ad', 'hsa_src', 'hsa_tgt', 'hsa_kw', 'hsa_mt', 'hsa_net', 'hsa_ver',
// Marketo
'mkt_tok',
// YouTube
'si', 'feature', 'kw', 'pp',
// Other common tracking
'ref', 'referrer', 'source', 'campaign', 'medium', 'content',
'yclid', 'srsltid', '_ga', '_gl', 'igshid', 'epik', 'pk_campaign', 'pk_kwd',
// Mobile app tracking
'adjust_tracker', 'adjust_campaign', 'adjust_adgroup', 'adjust_creative',
// Amazon
'tag', 'linkCode', 'creative', 'creativeASIN', 'linkId', 'ascsubtag',
// Affiliate tracking
'aff_id', 'affiliate_id', 'aff', 'ref_', 'refer',
// Social media share tracking
'share', 'shared', 'sharesource'
]
// Remove all tracking parameters
trackingParams.forEach(param => {
parsedUrl.searchParams.delete(param)
})
// Remove any parameter that starts with utm_
Array.from(parsedUrl.searchParams.keys()).forEach(key => {
if (key.startsWith('utm_') || key.startsWith('_')) {
parsedUrl.searchParams.delete(key)
}
})
return parsedUrl.toString()
} catch {
// If URL parsing fails, return original URL
return url
}
}

Loading…
Cancel
Save