From 2b8f5236d57ed2a520935a1c0d40941a5940f9f3 Mon Sep 17 00:00:00 2001 From: oabrivard Date: Mon, 23 Mar 2026 18:58:44 +0100 Subject: [PATCH] fix: strip smart quotes and zero-width chars from pasted URLs normalizeUrl now strips smart quotes, zero-width spaces, and other invisible formatting characters that browsers inject when copy-pasting URLs from rich text sources. Prevents false "URL invalid" errors for valid URLs. Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend/src/__tests__/sources-utils.test.ts | 15 +++++++++++++++ frontend/src/pages/Sources.tsx | 19 +++++++++++++------ 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/frontend/src/__tests__/sources-utils.test.ts b/frontend/src/__tests__/sources-utils.test.ts index 716cbce..a616d69 100644 --- a/frontend/src/__tests__/sources-utils.test.ts +++ b/frontend/src/__tests__/sources-utils.test.ts @@ -32,6 +32,21 @@ describe('normalizeUrl', () => { it('should handle URLs with www prefix', () => { expect(normalizeUrl('www.example.com')).toBe('https://www.example.com'); }); + + it('should strip smart quotes from copy-pasted URLs', () => { + expect(normalizeUrl('\u201Chttps://example.com\u201D')).toBe('https://example.com'); + expect(normalizeUrl('\u2018example.com\u2019')).toBe('https://example.com'); + }); + + it('should strip zero-width characters', () => { + expect(normalizeUrl('https://example.com\u200B')).toBe('https://example.com'); + expect(normalizeUrl('\uFEFFhttps://example.com')).toBe('https://example.com'); + }); + + it('should handle .ai TLD with path', () => { + expect(normalizeUrl('https://awesomeagents.ai/news/')).toBe('https://awesomeagents.ai/news/'); + expect(normalizeUrl('awesomeagents.ai/news/')).toBe('https://awesomeagents.ai/news/'); + }); }); describe('isValidUrl', () => { diff --git a/frontend/src/pages/Sources.tsx b/frontend/src/pages/Sources.tsx index bb71b32..19be72c 100644 --- a/frontend/src/pages/Sources.tsx +++ b/frontend/src/pages/Sources.tsx @@ -23,15 +23,22 @@ import LoadingSpinner from '~/components/ui/LoadingSpinner'; * Prepend https:// if the URL has no scheme. */ export function normalizeUrl(url: string): string { - const trimmed = url.trim(); - if (!trimmed) return trimmed; + // Strip smart quotes, zero-width chars, and other invisible formatting + // that browsers or rich-text editors inject when copy-pasting URLs + const cleaned = url + .trim() + .replace(/[\u200B-\u200D\uFEFF\u00A0]/g, '') // zero-width & non-breaking spaces + .replace(/^[\u201C\u201D\u201E\u201F\u2018\u2019\u00AB\u00BB"']+/, '') // leading quotes + .replace(/[\u201C\u201D\u201E\u201F\u2018\u2019\u00AB\u00BB"']+$/, '') // trailing quotes + .trim(); + if (!cleaned) return cleaned; if ( - !trimmed.startsWith('http://') && - !trimmed.startsWith('https://') + !cleaned.startsWith('http://') && + !cleaned.startsWith('https://') ) { - return 'https://' + trimmed; + return 'https://' + cleaned; } - return trimmed; + return cleaned; } /**