/* eslint-disable @next/next/no-img-element */ 'use client' import { displayUrlName } from '@/lib/utils/domain' import { faviconUrl } from '@/lib/utils/favicon' /** * Normalize a user- or model-supplied URL to a safe http(s) URL, or return * null if the scheme is not allowed. Prevents XSS via javascript:/data: URLs * embedded in model-generated spec blocks. */ const sanitizeHttpUrl = (raw: string | undefined): string | null => { if (!raw) return null try { const parsed = new URL(raw) if (parsed.protocol === 'http:' || parsed.protocol === 'https:') { return parsed.toString() } return null } catch { return null } } type ImageCreditOverlayProps = { /** Fallback link URL (e.g. the image URL itself). Used when sourceUrl is not set. */ url: string /** Original referring page URL; preferred for link/favicon/hostname when present. */ sourceUrl?: string title?: string description?: string } export function ImageCreditOverlay({ url, sourceUrl, title, description }: ImageCreditOverlayProps) { const safeLink = sanitizeHttpUrl(sourceUrl) ?? sanitizeHttpUrl(url) const favicon = safeLink ? faviconUrl(safeLink, 128) : '' const label = title || description const content = ( <> {favicon && ( )}
{safeLink && (
{displayUrlName(safeLink)}
)} {label && (
{label}
)}
) const className = 'absolute bottom-3 left-3 max-w-[80%] bg-black/70 backdrop-blur-sm rounded-xl px-3 py-2.5 flex items-center gap-2.5 no-underline hover:bg-black/80 transition-colors' if (!safeLink) { // Untrusted or missing URL: render a non-clickable label instead of an . return
{content}
} return (
e.stopPropagation()} > {content} ) }