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) <noreply@anthropic.com>
master
oabrivard 3 months ago
parent 45c9e71589
commit 2b8f5236d5

@ -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', () => {

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

Loading…
Cancel
Save