fix: use direct favicon.ico, open links in app mode (popup window)

- Favicon loaded directly from website's /favicon.ico instead of
  Google's service (which was unreliable/blocked on internal networks)
- Letter-initial fallback when favicon can't be loaded
- External links open in popup window (app mode) instead of new tab

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Thomas Reitz 2026-03-10 11:23:11 +01:00
parent 65c5c7b7dd
commit 0a52606012
2 changed files with 50 additions and 35 deletions

View file

@ -7,11 +7,11 @@ function generateId(): string {
return Date.now().toString(36) + Math.random().toString(36).slice(2, 10); return Date.now().toString(36) + Math.random().toString(36).slice(2, 10);
} }
/** Favicon-URL aus einer Website-URL ableiten (Google Favicon Service) */ /** Favicon-URL direkt von der Webseite laden */
function getFaviconUrl(url: string): string | null { function getFaviconUrl(url: string): string | null {
try { try {
const domain = new URL(url).hostname; const parsed = new URL(url);
return `https://www.google.com/s2/favicons?domain=${domain}&sz=32`; return `${parsed.origin}/favicon.ico`;
} catch { } catch {
return null; return null;
} }

View file

@ -12,11 +12,11 @@ interface ExternalLink {
sortOrder: number; sortOrder: number;
} }
/** Favicon-URL aus einer Website-URL ableiten (Google Favicon Service) */ /** Favicon-URL direkt von der Webseite laden */
function getFaviconUrl(url: string): string | null { function getFaviconUrl(url: string): string | null {
try { try {
const domain = new URL(url).hostname; const parsed = new URL(url);
return `https://www.google.com/s2/favicons?domain=${domain}&sz=32`; return `${parsed.origin}/favicon.ico`;
} catch { } catch {
return null; return null;
} }
@ -82,37 +82,52 @@ export function AppLayout() {
<a <a
key={link.id} key={link.id}
href={link.url} href={link.url}
target="_blank" onClick={(e) => {
rel="noopener noreferrer" e.preventDefault();
window.open(
link.url,
link.label,
'popup,noopener',
);
}}
className={styles.navLink} className={styles.navLink}
> >
{favicon ? ( <img
<img src={favicon || ''}
src={favicon} alt=""
alt="" style={{
style={{ width: 16,
width: 16, height: 16,
height: 16, objectFit: 'contain',
objectFit: 'contain', borderRadius: 2,
borderRadius: 2, }}
}} onError={(e) => {
/> // Fallback: erstes Zeichen des Labels als Text-Icon
) : ( const el = e.target as HTMLImageElement;
<svg el.style.display = 'none';
width="16" const fallback = el.nextElementSibling;
height="16" if (fallback)
viewBox="0 0 16 16" (fallback as HTMLElement).style.display =
fill="none" 'flex';
stroke="currentColor" }}
strokeWidth="1.5" />
strokeLinecap="round" <span
strokeLinejoin="round" style={{
> display: 'none',
<path d="M10 2h4v4" /> width: 16,
<path d="M6 10L14 2" /> height: 16,
<path d="M14 9v4a1 1 0 01-1 1H3a1 1 0 01-1-1V3a1 1 0 011-1h4" /> alignItems: 'center',
</svg> justifyContent: 'center',
)} fontSize: '0.625rem',
fontWeight: 700,
background: 'rgba(255,255,255,0.15)',
borderRadius: 2,
color: 'rgba(255,255,255,0.8)',
flexShrink: 0,
}}
>
{link.label.charAt(0).toUpperCase()}
</span>
{link.label} {link.label}
<svg <svg
width="10" width="10"