favicon-stealer 1.2.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -3
- package/dist/Favicon.js +29 -10
- package/dist/types/Favicon.d.ts +2 -1
- package/package.json +6 -1
- package/.gitattributes +0 -2
- package/src/Favicon.tsx +0 -134
- package/src/index.tsx +0 -1
- package/src/lib/utils/index.ts +0 -12
- package/tsconfig.json +0 -25
package/README.md
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
# favicon-stealer
|
|
2
|
-
favicon
|
|
2
|
+
A react component to get clear and consistent favicon of a website easily.
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
# installation
|
|
6
|
+
## npm
|
|
6
7
|
```shell
|
|
7
8
|
npm install favicon-stealer
|
|
8
9
|
```
|
|
10
|
+
## pnpm
|
|
11
|
+
```shell
|
|
12
|
+
pnpm add favicon-stealer
|
|
13
|
+
```
|
|
9
14
|
|
|
10
15
|
# usage
|
|
11
16
|
```typescript
|
|
@@ -20,22 +25,27 @@ import { Favicon } from 'favicon-stealer';
|
|
|
20
25
|
| `url` | `string` | The URL of the website to fetch the favicon for. |
|
|
21
26
|
| `size` | `number` | The size of the favicon in pixels. Default is 32. |
|
|
22
27
|
| `className` | `string` | A class name to apply to the element. |
|
|
23
|
-
| `timeout` | `number` | The timeout in milliseconds for fetching the favicon. Default is
|
|
28
|
+
| `timeout` | `number` | The timeout in milliseconds for fetching the favicon. Default is 3000 (3 seconds). |
|
|
24
29
|
| `lazy` | `boolean` | Whether to load the favicon lazily. Default is false. |
|
|
25
30
|
| `border` | `boolean` | Whether to show a border around the favicon. Default is false. |
|
|
26
31
|
| `padding` | `number` | The padding in pixels.(px) Default is 0. |
|
|
27
32
|
| `background` | `string` | The background color of the favicon. Default is transparent.(in hex) |
|
|
28
33
|
| `borderRadius` | `number` | The border radius in pixels.(px) Default is 0. |
|
|
34
|
+
| `preferGoogle` | `boolean` | Whether to prefer Google's favicon service over the website's own favicon. Default is false. |
|
|
35
|
+
|
|
29
36
|
|
|
30
37
|
# npm package
|
|
31
38
|
https://www.npmjs.com/package/favicon-stealer
|
|
32
39
|
|
|
40
|
+
|
|
33
41
|
# license
|
|
34
42
|
MIT License
|
|
35
43
|
|
|
44
|
+
|
|
36
45
|
# Changelog
|
|
37
46
|
- v1.0.0: Initial release (2025.1.21)
|
|
38
47
|
- v1.0.1: Add README.md (2025.1.21)
|
|
39
48
|
- v1.0.2: Update license to MIT (2025.1.21)
|
|
40
49
|
- v1.1.0: Fix show bug(2025.2.1)
|
|
41
|
-
- v1.2.0: Add props(lazy, border, padding, background, borderRadius)(2025.2.1)
|
|
50
|
+
- v1.2.0: Add props(lazy, border, padding, background, borderRadius)(2025.2.1)
|
|
51
|
+
- v1.4.0: Improved favicon detection and optimized package size; Added preferGoogle option and improved favicon loading reliability (2025.2.27)
|
package/dist/Favicon.js
CHANGED
|
@@ -36,25 +36,44 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
36
36
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
37
|
const utils_1 = require("./lib/utils");
|
|
38
38
|
const react_1 = __importStar(require("react"));
|
|
39
|
-
const Favicon = ({ url, size = 32, className = "", timeout =
|
|
40
|
-
border = false, padding = 0, background = "transparent", borderRadius = 0, lazy = false, }) => {
|
|
39
|
+
const Favicon = ({ url, size = 32, className = "", timeout = 5000, // 增加到5秒,给网站自己的favicon更多加载时间
|
|
40
|
+
border = false, padding = 0, background = "transparent", borderRadius = 0, lazy = false, preferGoogle = false, }) => {
|
|
41
41
|
const domain = (0, utils_1.getDomain)(url);
|
|
42
|
-
const [imgSrc, setImgSrc] = (0, react_1.useState)(
|
|
42
|
+
const [imgSrc, setImgSrc] = (0, react_1.useState)("");
|
|
43
43
|
const [fallbackIndex, setFallbackIndex] = (0, react_1.useState)(0);
|
|
44
44
|
const [isLoading, setIsLoading] = (0, react_1.useState)(true);
|
|
45
45
|
const [hasError, setHasError] = (0, react_1.useState)(false);
|
|
46
|
-
const
|
|
46
|
+
const [isInitialized, setIsInitialized] = (0, react_1.useState)(false);
|
|
47
|
+
const standardSources = [
|
|
48
|
+
`https://${domain}/favicon.ico`,
|
|
47
49
|
`https://${domain}/logo.svg`,
|
|
48
50
|
`https://${domain}/logo.png`,
|
|
49
51
|
`https://${domain}/apple-touch-icon.png`,
|
|
50
52
|
`https://${domain}/apple-touch-icon-precomposed.png`,
|
|
51
|
-
`https
|
|
53
|
+
`https://${domain}/static/img/favicon.ico`,
|
|
54
|
+
`https://${domain}/static/img/favicon.png`,
|
|
55
|
+
`https://${domain}/img/favicon.png`,
|
|
56
|
+
`https://${domain}/img/favicon.ico`,
|
|
57
|
+
`https://${domain}/static/img/logo.svg`,
|
|
58
|
+
`https://${domain}/apple-touch-icon-precomposed.png`,
|
|
59
|
+
];
|
|
60
|
+
const fallbackServices = [
|
|
61
|
+
`https://www.google.com/s2/favicons?domain=https://${domain}&sz=64`,
|
|
62
|
+
`https://www.google.com/s2/favicons?domain=http://${domain}&sz=64`,
|
|
52
63
|
`https://icons.duckduckgo.com/ip3/${domain}.ico`,
|
|
53
|
-
`https://${domain}/favicon.ico`,
|
|
54
64
|
];
|
|
65
|
+
const fallbackSources = preferGoogle
|
|
66
|
+
? [...fallbackServices, ...standardSources]
|
|
67
|
+
: [...standardSources, ...fallbackServices];
|
|
68
|
+
(0, react_1.useEffect)(() => {
|
|
69
|
+
if (!isInitialized) {
|
|
70
|
+
setImgSrc(fallbackSources[0]);
|
|
71
|
+
setIsInitialized(true);
|
|
72
|
+
}
|
|
73
|
+
}, [isInitialized, fallbackSources]);
|
|
55
74
|
(0, react_1.useEffect)(() => {
|
|
56
75
|
let timeoutId;
|
|
57
|
-
if (isLoading) {
|
|
76
|
+
if (isLoading && imgSrc) {
|
|
58
77
|
timeoutId = setTimeout(() => {
|
|
59
78
|
handleError();
|
|
60
79
|
}, timeout);
|
|
@@ -64,7 +83,7 @@ border = false, padding = 0, background = "transparent", borderRadius = 0, lazy
|
|
|
64
83
|
clearTimeout(timeoutId);
|
|
65
84
|
}
|
|
66
85
|
};
|
|
67
|
-
}, [imgSrc, isLoading]);
|
|
86
|
+
}, [imgSrc, isLoading, timeout]);
|
|
68
87
|
const handleError = () => {
|
|
69
88
|
const nextIndex = fallbackIndex + 1;
|
|
70
89
|
if (nextIndex < fallbackSources.length) {
|
|
@@ -96,10 +115,10 @@ border = false, padding = 0, background = "transparent", borderRadius = 0, lazy
|
|
|
96
115
|
} },
|
|
97
116
|
isLoading && (react_1.default.createElement("div", { className: "absolute inset-0 animate-pulse" },
|
|
98
117
|
react_1.default.createElement("div", { className: "w-full h-full rounded-md bg-gray-200/60" }))),
|
|
99
|
-
react_1.default.createElement("img", { src: imgSrc, alt: `${domain} logo`, width: size, height: size, loading: lazy ? "lazy" : "eager", onError: handleError, onLoad: handleLoad, className: `inline-block transition-opacity duration-300 ${isLoading ? "opacity-0" : "opacity-100"}`, style: {
|
|
118
|
+
imgSrc && (react_1.default.createElement("img", { src: imgSrc, alt: `${domain} logo`, width: size, height: size, loading: lazy ? "lazy" : "eager", onError: handleError, onLoad: handleLoad, className: `inline-block transition-opacity duration-300 ${isLoading ? "opacity-0" : "opacity-100"}`, style: {
|
|
100
119
|
objectFit: "contain",
|
|
101
120
|
display: hasError ? "none" : "inline-block",
|
|
102
|
-
} }),
|
|
121
|
+
} })),
|
|
103
122
|
hasError && (react_1.default.createElement("div", { className: "w-full h-full flex items-center justify-center bg-gray-100 rounded-md", style: { fontSize: `${size * 0.5}px` } }, domain.charAt(0).toUpperCase()))));
|
|
104
123
|
};
|
|
105
124
|
exports.default = Favicon;
|
package/dist/types/Favicon.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ interface IProps {
|
|
|
9
9
|
background?: string;
|
|
10
10
|
borderRadius?: number;
|
|
11
11
|
lazy?: boolean;
|
|
12
|
+
preferGoogle?: boolean;
|
|
12
13
|
}
|
|
13
|
-
declare const Favicon: ({ url, size, className, timeout, border, padding, background, borderRadius, lazy, }: IProps) => React.JSX.Element;
|
|
14
|
+
declare const Favicon: ({ url, size, className, timeout, border, padding, background, borderRadius, lazy, preferGoogle, }: IProps) => React.JSX.Element;
|
|
14
15
|
export default Favicon;
|
package/package.json
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "favicon-stealer",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"types": "dist/types/index.d.ts",
|
|
6
|
+
"files": [
|
|
7
|
+
"dist",
|
|
8
|
+
"LICENSE",
|
|
9
|
+
"README.md"
|
|
10
|
+
],
|
|
6
11
|
"scripts": {
|
|
7
12
|
"test": "echo \"Error: no test specified\" && exit 1",
|
|
8
13
|
"build": "tsc",
|
package/.gitattributes
DELETED
package/src/Favicon.tsx
DELETED
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { getDomain } from "./lib/utils";
|
|
4
|
-
import React, { useEffect, useState } from "react";
|
|
5
|
-
|
|
6
|
-
interface IProps {
|
|
7
|
-
url: string;
|
|
8
|
-
size?: number;
|
|
9
|
-
className?: string;
|
|
10
|
-
timeout?: number;
|
|
11
|
-
border?: boolean;
|
|
12
|
-
padding?: number;
|
|
13
|
-
background?: string;
|
|
14
|
-
borderRadius?: number;
|
|
15
|
-
lazy?: boolean;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const Favicon = ({
|
|
19
|
-
url,
|
|
20
|
-
size = 32,
|
|
21
|
-
className = "",
|
|
22
|
-
timeout = 1000, // 1 second
|
|
23
|
-
border = false,
|
|
24
|
-
padding = 0,
|
|
25
|
-
background = "transparent",
|
|
26
|
-
borderRadius = 0,
|
|
27
|
-
lazy = false,
|
|
28
|
-
}: IProps) => {
|
|
29
|
-
const domain = getDomain(url);
|
|
30
|
-
const [imgSrc, setImgSrc] = useState(`https://${domain}/logo.svg`);
|
|
31
|
-
const [fallbackIndex, setFallbackIndex] = useState(0);
|
|
32
|
-
const [isLoading, setIsLoading] = useState(true);
|
|
33
|
-
const [hasError, setHasError] = useState(false);
|
|
34
|
-
|
|
35
|
-
const fallbackSources = [
|
|
36
|
-
`https://${domain}/logo.svg`,
|
|
37
|
-
`https://${domain}/logo.png`,
|
|
38
|
-
`https://${domain}/apple-touch-icon.png`,
|
|
39
|
-
`https://${domain}/apple-touch-icon-precomposed.png`,
|
|
40
|
-
`https://www.google.com/s2/favicons?domain=${domain}&sz=64`,
|
|
41
|
-
`https://icons.duckduckgo.com/ip3/${domain}.ico`,
|
|
42
|
-
`https://${domain}/favicon.ico`,
|
|
43
|
-
];
|
|
44
|
-
|
|
45
|
-
useEffect(() => {
|
|
46
|
-
let timeoutId: any;
|
|
47
|
-
|
|
48
|
-
if (isLoading) {
|
|
49
|
-
timeoutId = setTimeout(() => {
|
|
50
|
-
handleError();
|
|
51
|
-
}, timeout);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
return () => {
|
|
55
|
-
if (timeoutId) {
|
|
56
|
-
clearTimeout(timeoutId);
|
|
57
|
-
}
|
|
58
|
-
};
|
|
59
|
-
}, [imgSrc, isLoading]);
|
|
60
|
-
|
|
61
|
-
const handleError = () => {
|
|
62
|
-
const nextIndex = fallbackIndex + 1;
|
|
63
|
-
if (nextIndex < fallbackSources.length) {
|
|
64
|
-
setFallbackIndex(nextIndex);
|
|
65
|
-
setImgSrc(fallbackSources[nextIndex]);
|
|
66
|
-
setIsLoading(true);
|
|
67
|
-
} else {
|
|
68
|
-
setHasError(true);
|
|
69
|
-
setIsLoading(false);
|
|
70
|
-
}
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
const handleLoad = () => {
|
|
74
|
-
setIsLoading(false);
|
|
75
|
-
setHasError(false);
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
return (
|
|
79
|
-
<div
|
|
80
|
-
className={
|
|
81
|
-
`relative inline-block
|
|
82
|
-
${className}
|
|
83
|
-
${border ? "border" : ""}
|
|
84
|
-
${hasError ? "opacity-0" : ""}
|
|
85
|
-
${padding ? `p-[${padding}px]` : ""}
|
|
86
|
-
${borderRadius ? `rounded-[${borderRadius}px]` : ""}
|
|
87
|
-
`
|
|
88
|
-
}
|
|
89
|
-
style={{
|
|
90
|
-
width: size,
|
|
91
|
-
height: size,
|
|
92
|
-
background: background,
|
|
93
|
-
padding: padding ? `${padding}px` : 0,
|
|
94
|
-
borderRadius: borderRadius ? `${borderRadius}px` : 0,
|
|
95
|
-
}}
|
|
96
|
-
>
|
|
97
|
-
{/* placeholder */}
|
|
98
|
-
{isLoading && (
|
|
99
|
-
<div className="absolute inset-0 animate-pulse">
|
|
100
|
-
<div className="w-full h-full rounded-md bg-gray-200/60" />
|
|
101
|
-
</div>
|
|
102
|
-
)}
|
|
103
|
-
|
|
104
|
-
<img
|
|
105
|
-
src={imgSrc}
|
|
106
|
-
alt={`${domain} logo`}
|
|
107
|
-
width={size}
|
|
108
|
-
height={size}
|
|
109
|
-
loading={lazy ? "lazy" : "eager"}
|
|
110
|
-
onError={handleError}
|
|
111
|
-
onLoad={handleLoad}
|
|
112
|
-
className={`inline-block transition-opacity duration-300 ${
|
|
113
|
-
isLoading ? "opacity-0" : "opacity-100"
|
|
114
|
-
}`}
|
|
115
|
-
style={{
|
|
116
|
-
objectFit: "contain",
|
|
117
|
-
display: hasError ? "none" : "inline-block",
|
|
118
|
-
}}
|
|
119
|
-
/>
|
|
120
|
-
|
|
121
|
-
{/* Fallback: Display first letter of domain when all image sources fail */}
|
|
122
|
-
{hasError && (
|
|
123
|
-
<div
|
|
124
|
-
className="w-full h-full flex items-center justify-center bg-gray-100 rounded-md"
|
|
125
|
-
style={{ fontSize: `${size * 0.5}px` }}
|
|
126
|
-
>
|
|
127
|
-
{domain.charAt(0).toUpperCase()}
|
|
128
|
-
</div>
|
|
129
|
-
)}
|
|
130
|
-
</div>
|
|
131
|
-
);
|
|
132
|
-
};
|
|
133
|
-
|
|
134
|
-
export default Favicon;
|
package/src/index.tsx
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { default as Favicon } from './Favicon';
|
package/src/lib/utils/index.ts
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
export const getDomain = (url: string) => {
|
|
2
|
-
try {
|
|
3
|
-
// Add https:// protocol if not present
|
|
4
|
-
const urlWithProtocol = url.startsWith('http') ? url : `https://${url}`;
|
|
5
|
-
const domain = new URL(urlWithProtocol).hostname;
|
|
6
|
-
// Remove 'www.' prefix if exists
|
|
7
|
-
return domain.replace(/^www\./, '');
|
|
8
|
-
} catch (error) {
|
|
9
|
-
// Return original input if URL parsing fails
|
|
10
|
-
return url;
|
|
11
|
-
}
|
|
12
|
-
};
|
package/tsconfig.json
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"outDir": "dist",
|
|
4
|
-
"module": "commonjs",
|
|
5
|
-
"target": "es2015",
|
|
6
|
-
"jsx": "react",
|
|
7
|
-
"declaration": true,
|
|
8
|
-
"declarationDir": "dist/types",
|
|
9
|
-
"strict": true,
|
|
10
|
-
"moduleResolution": "node",
|
|
11
|
-
"baseUrl": "./src",
|
|
12
|
-
"rootDir": "./src",
|
|
13
|
-
"paths": {
|
|
14
|
-
"@/*": ["./*"]
|
|
15
|
-
},
|
|
16
|
-
"esModuleInterop": true
|
|
17
|
-
},
|
|
18
|
-
"include": [
|
|
19
|
-
"src"
|
|
20
|
-
],
|
|
21
|
-
"exclude": [
|
|
22
|
-
"node_modules",
|
|
23
|
-
"dist"
|
|
24
|
-
]
|
|
25
|
-
}
|