Handle malformed article URLs
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { Article, ReaderSettings } from '../types';
|
||||
import { FileText, MousePointerClick } from 'lucide-react';
|
||||
import { getDisplayUrl } from '../utils/url';
|
||||
|
||||
interface ReaderViewProps {
|
||||
article?: Article | null;
|
||||
@@ -68,6 +69,8 @@ export const ReaderView: React.FC<ReaderViewProps> = ({ article, settings, onTog
|
||||
);
|
||||
}
|
||||
|
||||
const displayUrl = getDisplayUrl(article.url);
|
||||
|
||||
return (
|
||||
<div className="bg-white dark:bg-slate-900 rounded-2xl border border-slate-200 dark:border-slate-800 shadow-sm overflow-hidden h-[calc(100vh-12rem)] flex flex-col transition-colors duration-300">
|
||||
<div className="p-6 border-b border-slate-100 dark:border-slate-800 bg-white dark:bg-slate-900 sticky top-0 z-10 flex justify-between items-start">
|
||||
@@ -76,12 +79,12 @@ export const ReaderView: React.FC<ReaderViewProps> = ({ article, settings, onTog
|
||||
{article.title}
|
||||
</h2>
|
||||
<a
|
||||
href={article.url}
|
||||
href={displayUrl.href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-sm text-blue-600 dark:text-blue-400 hover:underline mt-2 inline-block"
|
||||
>
|
||||
{new URL(article.url).hostname}
|
||||
{displayUrl.hostname}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
|
||||
2966
package-lock.json
generated
Normal file
2966
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,8 @@
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
"preview": "vite preview",
|
||||
"test": "vitest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@google/genai": "^1.30.0",
|
||||
@@ -20,6 +21,7 @@
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"typescript": "^5.7.2",
|
||||
"vite": "^6.0.0"
|
||||
"vite": "^6.0.0",
|
||||
"vitest": "^4.0.14"
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { GoogleGenAI, Modality } from '@google/genai';
|
||||
import { VoiceName } from '../types';
|
||||
import { normalizeUrl } from '../utils/url';
|
||||
|
||||
const getAiClient = () => {
|
||||
const apiKey = process.env.API_KEY;
|
||||
@@ -9,18 +10,6 @@ const getAiClient = () => {
|
||||
return new GoogleGenAI({ apiKey });
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper to ensure URL has protocol.
|
||||
* Proxies often fail if 'http/https' is missing.
|
||||
*/
|
||||
const normalizeUrl = (url: string) => {
|
||||
let cleanUrl = url.trim();
|
||||
if (!cleanUrl.startsWith('http://') && !cleanUrl.startsWith('https://')) {
|
||||
return `https://${cleanUrl}`;
|
||||
}
|
||||
return cleanUrl;
|
||||
};
|
||||
|
||||
/**
|
||||
* List of CORS proxies to try in order.
|
||||
* This improves reliability if one service is down or blocked.
|
||||
|
||||
28
utils/url.test.ts
Normal file
28
utils/url.test.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { getDisplayUrl, normalizeUrl } from './url';
|
||||
|
||||
describe('normalizeUrl', () => {
|
||||
it('adds https protocol when missing', () => {
|
||||
expect(normalizeUrl('example.com/page')).toBe('https://example.com/page');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDisplayUrl', () => {
|
||||
it('returns hostname and normalized href for valid URLs', () => {
|
||||
const result = getDisplayUrl('https://example.com/path');
|
||||
expect(result.hostname).toBe('example.com');
|
||||
expect(result.href).toBe('https://example.com/path');
|
||||
});
|
||||
|
||||
it('normalizes URLs without protocol for display', () => {
|
||||
const result = getDisplayUrl('example.com/path');
|
||||
expect(result.hostname).toBe('example.com');
|
||||
expect(result.href).toBe('https://example.com/path');
|
||||
});
|
||||
|
||||
it('falls back to raw URL when parsing fails', () => {
|
||||
const result = getDisplayUrl('not a url');
|
||||
expect(result.hostname).toBe('not a url');
|
||||
expect(result.href).toBe('not a url');
|
||||
});
|
||||
});
|
||||
23
utils/url.ts
Normal file
23
utils/url.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
export const normalizeUrl = (url: string) => {
|
||||
let cleanUrl = url.trim();
|
||||
if (!cleanUrl.startsWith('http://') && !cleanUrl.startsWith('https://')) {
|
||||
return `https://${cleanUrl}`;
|
||||
}
|
||||
return cleanUrl;
|
||||
};
|
||||
|
||||
export const getDisplayUrl = (url: string): { href: string; hostname: string } => {
|
||||
const normalized = normalizeUrl(url);
|
||||
|
||||
try {
|
||||
const parsed = new URL(normalized);
|
||||
return { href: normalized, hostname: parsed.hostname };
|
||||
} catch {
|
||||
try {
|
||||
const fallback = new URL(url);
|
||||
return { href: url, hostname: fallback.hostname };
|
||||
} catch {
|
||||
return { href: url, hostname: url };
|
||||
}
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user