mirror of
https://github.com/Tony0410/News-reader-pro.git
synced 2026-05-24 21:31:44 +08:00
Handle malformed article URLs
This commit is contained in:
@@ -2,6 +2,7 @@
|
|||||||
import React, { useEffect, useRef } from 'react';
|
import React, { useEffect, useRef } from 'react';
|
||||||
import { Article, ReaderSettings } from '../types';
|
import { Article, ReaderSettings } from '../types';
|
||||||
import { FileText, MousePointerClick } from 'lucide-react';
|
import { FileText, MousePointerClick } from 'lucide-react';
|
||||||
|
import { getDisplayUrl } from '../utils/url';
|
||||||
|
|
||||||
interface ReaderViewProps {
|
interface ReaderViewProps {
|
||||||
article?: Article | null;
|
article?: Article | null;
|
||||||
@@ -68,6 +69,8 @@ export const ReaderView: React.FC<ReaderViewProps> = ({ article, settings, onTog
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const displayUrl = getDisplayUrl(article.url);
|
||||||
|
|
||||||
return (
|
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="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">
|
<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}
|
{article.title}
|
||||||
</h2>
|
</h2>
|
||||||
<a
|
<a
|
||||||
href={article.url}
|
href={displayUrl.href}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="text-sm text-blue-600 dark:text-blue-400 hover:underline mt-2 inline-block"
|
className="text-sm text-blue-600 dark:text-blue-400 hover:underline mt-2 inline-block"
|
||||||
>
|
>
|
||||||
{new URL(article.url).hostname}
|
{displayUrl.hostname}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</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": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview",
|
||||||
|
"test": "vitest"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@google/genai": "^1.30.0",
|
"@google/genai": "^1.30.0",
|
||||||
@@ -20,6 +21,7 @@
|
|||||||
"@types/uuid": "^10.0.0",
|
"@types/uuid": "^10.0.0",
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
"typescript": "^5.7.2",
|
"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 { GoogleGenAI, Modality } from '@google/genai';
|
||||||
import { VoiceName } from '../types';
|
import { VoiceName } from '../types';
|
||||||
|
import { normalizeUrl } from '../utils/url';
|
||||||
|
|
||||||
const getAiClient = () => {
|
const getAiClient = () => {
|
||||||
const apiKey = process.env.API_KEY;
|
const apiKey = process.env.API_KEY;
|
||||||
@@ -9,18 +10,6 @@ const getAiClient = () => {
|
|||||||
return new GoogleGenAI({ apiKey });
|
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.
|
* List of CORS proxies to try in order.
|
||||||
* This improves reliability if one service is down or blocked.
|
* 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