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,13 +82,18 @@ 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,
@ -96,23 +101,33 @@ export function AppLayout() {
objectFit: 'contain', objectFit: 'contain',
borderRadius: 2, borderRadius: 2,
}} }}
onError={(e) => {
// Fallback: erstes Zeichen des Labels als Text-Icon
const el = e.target as HTMLImageElement;
el.style.display = 'none';
const fallback = el.nextElementSibling;
if (fallback)
(fallback as HTMLElement).style.display =
'flex';
}}
/> />
) : ( <span
<svg style={{
width="16" display: 'none',
height="16" width: 16,
viewBox="0 0 16 16" height: 16,
fill="none" alignItems: 'center',
stroke="currentColor" justifyContent: 'center',
strokeWidth="1.5" fontSize: '0.625rem',
strokeLinecap="round" fontWeight: 700,
strokeLinejoin="round" background: 'rgba(255,255,255,0.15)',
borderRadius: 2,
color: 'rgba(255,255,255,0.8)',
flexShrink: 0,
}}
> >
<path d="M10 2h4v4" /> {link.label.charAt(0).toUpperCase()}
<path d="M6 10L14 2" /> </span>
<path d="M14 9v4a1 1 0 01-1 1H3a1 1 0 01-1-1V3a1 1 0 011-1h4" />
</svg>
)}
{link.label} {link.label}
<svg <svg
width="10" width="10"