add gray fallback for missing feed favicons

This commit is contained in:
Yuan
2026-02-10 22:48:35 +08:00
parent f263c4875f
commit e1dcdcb5e6
7 changed files with 69 additions and 33 deletions
@@ -30,6 +30,7 @@ import { useArticleNavigation } from "@/hooks/use-keyboard";
import { formatDate } from "@/lib/utils";
import { processArticleContent } from "@/lib/content";
import { getFaviconUrl } from "@/lib/api/favicon";
import { FeedFavicon } from "@/components/feed/feed-favicon";
export function ArticleDrawer() {
const {
@@ -206,11 +207,9 @@ export function ArticleDrawer() {
<div className="flex flex-wrap items-center gap-x-2 gap-y-1 text-sm">
<span className="flex max-w-48 items-center gap-1.5 rounded bg-muted px-2 py-1 text-xs font-medium text-muted-foreground">
{feed && (
<img
<FeedFavicon
src={getFaviconUrl(feed.link, feed.site_url)}
alt=""
className="h-3.5 w-3.5 shrink-0 rounded-sm"
loading="lazy"
className="h-3.5 w-3.5 rounded-sm"
/>
)}
<span className="truncate">
@@ -2,6 +2,7 @@ import { Circle, CircleCheck, Star, ExternalLink } from "lucide-react";
import { Button } from "@/components/ui/button";
import { cn, formatDate, extractSummary } from "@/lib/utils";
import type { Item } from "@/lib/api";
import { FeedFavicon } from "@/components/feed/feed-favicon";
interface ArticleItemProps {
article: Item;
@@ -86,14 +87,7 @@ export function ArticleItem({
{extractSummary(article.content, 150)}
</p>
<div className="flex items-center gap-2 text-xs">
{feedFaviconUrl && (
<img
src={feedFaviconUrl}
alt=""
className="h-3.5 w-3.5 shrink-0 rounded-sm"
loading="lazy"
/>
)}
<FeedFavicon src={feedFaviconUrl} className="h-3.5 w-3.5 rounded-sm" />
<span className="truncate font-medium text-muted-foreground">
{feedName}
</span>
@@ -0,0 +1,37 @@
import { useEffect, useState } from "react";
import { cn } from "@/lib/utils";
interface FeedFaviconProps {
src?: string | null;
className?: string;
}
export function FeedFavicon({ src, className }: FeedFaviconProps) {
const [loadFailed, setLoadFailed] = useState(!src);
useEffect(() => {
setLoadFailed(!src);
}, [src]);
if (!src || loadFailed) {
return (
<span
aria-hidden="true"
className={cn(
"inline-block shrink-0 rounded bg-gray-300 dark:bg-gray-600",
className,
)}
/>
);
}
return (
<img
src={src}
alt=""
className={cn("shrink-0 rounded", className)}
loading="lazy"
onError={() => setLoadFailed(true)}
/>
);
}
+2 -6
View File
@@ -3,6 +3,7 @@ import { useUrlState } from "@/hooks/use-url-state";
import { useUIStore } from "@/store";
import { getFaviconUrl } from "@/lib/api/favicon";
import type { Feed } from "@/lib/api";
import { FeedFavicon } from "@/components/feed/feed-favicon";
import { Settings } from "lucide-react";
import { Button } from "@/components/ui/button";
@@ -30,12 +31,7 @@ export function FeedItem({ feed }: FeedItemProps) {
isSelected ? "bg-accent text-accent-foreground" : "hover:bg-accent/50",
)}
>
<img
src={faviconUrl}
alt=""
className="h-4 w-4 shrink-0 rounded"
loading="lazy"
/>
<FeedFavicon src={faviconUrl} className="h-4 w-4" />
<span className="block min-w-0 max-w-full flex-1 truncate">
{feed.name}
</span>
@@ -24,6 +24,7 @@ import { useUIStore } from "@/store";
import { useFeedLookup } from "@/queries/feeds";
import { useUrlState } from "@/hooks/use-url-state";
import { formatDate } from "@/lib/utils";
import { FeedFavicon } from "@/components/feed/feed-favicon";
export function SearchDialog() {
const { isSearchOpen, setSearchOpen, setEditFeedOpen } = useUIStore();
@@ -142,11 +143,9 @@ export function SearchDialog() {
onSelect={() => handleSelectFeed(feed.id)}
className="group gap-2"
>
<img
<FeedFavicon
src={getFaviconUrl(feed.link, feed.site_url)}
alt=""
className="h-4 w-4 shrink-0 rounded-sm"
loading="lazy"
className="h-4 w-4 rounded-sm"
/>
<span className="flex-1 truncate">{feed.name}</span>
<Button
+5 -1
View File
@@ -35,6 +35,10 @@ export function getFaviconUrl(feedLink: string, siteUrl?: string): string {
domain = extractDomainFromFeedLink(feedLink);
}
if (!domain) {
return "";
}
return `https://www.google.com/s2/favicons?domain=${domain}&sz=32`;
}
@@ -53,6 +57,6 @@ function extractDomainFromFeedLink(feedLink: string): string {
return hostname;
} catch {
return "example.com";
return "";
}
}
+17 -10
View File
@@ -47,6 +47,7 @@ import {
} from "@/queries/feeds";
import { useDeleteGroup, useGroups, useUpdateGroup } from "@/queries/groups";
import { useUIStore } from "@/store";
import { FeedFavicon } from "@/components/feed/feed-favicon";
export const Route = createLazyFileRoute("/feeds")({
component: FeedsPage,
@@ -246,11 +247,16 @@ function FeedsPage() {
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
{(Object.keys(statusFilterLabels) as StatusFilter[]).map((key) => (
<DropdownMenuItem key={key} onSelect={() => setStatusFilter(key)}>
{statusFilterLabels[key]}
</DropdownMenuItem>
))}
{(Object.keys(statusFilterLabels) as StatusFilter[]).map(
(key) => (
<DropdownMenuItem
key={key}
onSelect={() => setStatusFilter(key)}
>
{statusFilterLabels[key]}
</DropdownMenuItem>
),
)}
</DropdownMenuContent>
</DropdownMenu>
</div>
@@ -317,7 +323,10 @@ function FeedsPage() {
const isEditing = editingGroupId === group.id;
return (
<div key={group.id} className="overflow-hidden rounded-lg border">
<div
key={group.id}
className="overflow-hidden rounded-lg border"
>
<div
onClick={() => {
if (!isEditing) toggleGroup(group.id);
@@ -421,11 +430,9 @@ function FeedsPage() {
)}
>
<div className="flex min-w-0 flex-1 items-center gap-2.5">
<img
<FeedFavicon
src={getFaviconUrl(feed.link, feed.site_url)}
alt=""
className="h-5 w-5 shrink-0 rounded"
loading="lazy"
className="h-5 w-5"
/>
<div className="min-w-0">
<p className="truncate text-[13px] font-medium">