From 01f8685d91dd2f973a45853adb3bc4e1bac473ab Mon Sep 17 00:00:00 2001 From: Silberengel Date: Mon, 13 Oct 2025 10:38:24 +0200 Subject: [PATCH] remove trackers when previewing, publishing, or rendering --- src/components/Embedded/EmbeddedNormalUrl.tsx | 9 ++- src/components/PostEditor/HighlightEditor.tsx | 7 +- src/components/PostEditor/PostContent.tsx | 25 +++++-- .../PostEditor/PostTextarea/Preview.tsx | 16 +++- src/lib/url.ts | 75 +++++++++++++++++++ 5 files changed, 120 insertions(+), 12 deletions(-) diff --git a/src/components/Embedded/EmbeddedNormalUrl.tsx b/src/components/Embedded/EmbeddedNormalUrl.tsx index 7d43b82..edd8781 100644 --- a/src/components/Embedded/EmbeddedNormalUrl.tsx +++ b/src/components/Embedded/EmbeddedNormalUrl.tsx @@ -1,13 +1,18 @@ +import { cleanUrl } from '@/lib/url' + export function EmbeddedNormalUrl({ url }: { url: string }) { + // Clean tracking parameters from URLs before displaying/linking + const cleanedUrl = cleanUrl(url) + return ( e.stopPropagation()} rel="noreferrer" > - {url} + {cleanedUrl} ) } diff --git a/src/components/PostEditor/HighlightEditor.tsx b/src/components/PostEditor/HighlightEditor.tsx index 5a22b99..2ca2f25 100644 --- a/src/components/PostEditor/HighlightEditor.tsx +++ b/src/components/PostEditor/HighlightEditor.tsx @@ -2,6 +2,7 @@ import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Textarea } from '@/components/ui/textarea' +import { cleanUrl } from '@/lib/url' import { X } from 'lucide-react' import { useState, useEffect } from 'react' import { useTranslation } from 'react-i18next' @@ -38,11 +39,13 @@ export default function HighlightEditor({ } // 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('') setHighlightData({ sourceType: 'url', - sourceValue: sourceInput, + sourceValue: cleanedUrl, context }) return diff --git a/src/components/PostEditor/PostContent.tsx b/src/components/PostEditor/PostContent.tsx index dce6861..f06970b 100644 --- a/src/components/PostEditor/PostContent.tsx +++ b/src/components/PostEditor/PostContent.tsx @@ -15,7 +15,7 @@ import { isTouchDevice } from '@/lib/utils' import { useNostr } from '@/providers/NostrProvider' import { useFeed } from '@/providers/FeedProvider' 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 { TPollCreateData } from '@/types' import { ImageUp, ListTodo, LoaderCircle, MessageCircle, Settings, Smile, X, Highlighter } from 'lucide-react' @@ -190,12 +190,23 @@ export default function PostContent({ let newEvent: any = null 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) { // For highlights, pass the original sourceValue which contains the full identifier // The createHighlightDraftEvent function will parse it correctly draftEvent = await createHighlightDraftEvent( - text, + cleanedText, highlightData.sourceType, highlightData.sourceValue, highlightData.context, @@ -206,28 +217,28 @@ export default function PostContent({ } ) } else if (isPublicMessage) { - draftEvent = await createPublicMessageDraftEvent(text, extractedMentions, { + draftEvent = await createPublicMessageDraftEvent(cleanedText, extractedMentions, { addClientTag, isNsfw }) } else if (parentEvent && parentEvent.kind === ExtendedKind.PUBLIC_MESSAGE) { - draftEvent = await createPublicMessageReplyDraftEvent(text, parentEvent, mentions, { + draftEvent = await createPublicMessageReplyDraftEvent(cleanedText, parentEvent, mentions, { addClientTag, isNsfw }) } else if (parentEvent && parentEvent.kind !== kinds.ShortTextNote) { - draftEvent = await createCommentDraftEvent(text, parentEvent, mentions, { + draftEvent = await createCommentDraftEvent(cleanedText, parentEvent, mentions, { addClientTag, protectedEvent: isProtectedEvent, isNsfw }) } else if (isPoll) { - draftEvent = await createPollDraftEvent(pubkey!, text, mentions, pollCreateData, { + draftEvent = await createPollDraftEvent(pubkey!, cleanedText, mentions, pollCreateData, { addClientTag, isNsfw }) } else { - draftEvent = await createShortTextNoteDraftEvent(text, mentions, { + draftEvent = await createShortTextNoteDraftEvent(cleanedText, mentions, { parentEvent, addClientTag, protectedEvent: isProtectedEvent, diff --git a/src/components/PostEditor/PostTextarea/Preview.tsx b/src/components/PostEditor/PostTextarea/Preview.tsx index a727191..100f675 100644 --- a/src/components/PostEditor/PostTextarea/Preview.tsx +++ b/src/components/PostEditor/PostTextarea/Preview.tsx @@ -1,6 +1,7 @@ import { Card } from '@/components/ui/card' import { transformCustomEmojisInContent } from '@/lib/draft-event' import { createFakeEvent } from '@/lib/event' +import { cleanUrl } from '@/lib/url' import { cn } from '@/lib/utils' import { useMemo } from 'react' import Content from '../../Content' @@ -15,7 +16,20 @@ export default function Preview({ kind?: number }) { 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] ) return ( diff --git a/src/lib/url.ts b/src/lib/url.ts index 8e002eb..9f92929 100644 --- a/src/lib/url.ts +++ b/src/lib/url.ts @@ -144,3 +144,78 @@ export function isMedia(url: string) { 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 + } +}