Handle malformed article URLs

This commit is contained in:
Anthony
2025-11-27 21:18:43 +08:00
parent 7f75b44af1
commit 061474c574
6 changed files with 3030 additions and 19 deletions

View File

@@ -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">
@@ -75,13 +78,13 @@ export const ReaderView: React.FC<ReaderViewProps> = ({ article, settings, onTog
<h2 className="text-2xl font-bold text-slate-900 dark:text-slate-100 leading-tight">
{article.title}
</h2>
<a
href={article.url}
target="_blank"
<a
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

File diff suppressed because it is too large Load Diff

View File

@@ -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"
}
}
}

View File

@@ -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
View 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
View 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 };
}
}
};