mirror of
https://github.com/Sonny93/my-links.git
synced 2025-12-10 15:35:35 +00:00
fix: some cases where favicon are broken
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -18,6 +18,8 @@
|
|||||||
# misc
|
# misc
|
||||||
.DS_Store
|
.DS_Store
|
||||||
*.pem
|
*.pem
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
|
||||||
# debug
|
# debug
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
|
|||||||
@@ -1,25 +1,29 @@
|
|||||||
const { i18n } = require("./next-i18next.config");
|
const { i18n } = require("./next-i18next.config");
|
||||||
|
|
||||||
/** @type {import('next').NextConfig} */
|
/** @type {import("next").NextConfig} */
|
||||||
const config = {
|
const config = {
|
||||||
i18n,
|
i18n,
|
||||||
webpack(config) {
|
webpack(config) {
|
||||||
config.module.rules.push({
|
config.module.rules.push({
|
||||||
test: /\.svg$/,
|
test: /\.svg$/,
|
||||||
use: ["@svgr/webpack"],
|
use: ["@svgr/webpack"]
|
||||||
});
|
});
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
},
|
},
|
||||||
images: {
|
images: {
|
||||||
domains: ["localhost", "t3.gstatic.com", "lh3.googleusercontent.com"],
|
remotePatterns: [
|
||||||
formats: ["image/webp"],
|
{ hostname: "localhost" },
|
||||||
|
{ hostname: "t3.gstatic.com" },
|
||||||
|
{ hostname: "lh3.googleusercontent.com" }
|
||||||
|
],
|
||||||
|
formats: ["image/webp"]
|
||||||
},
|
},
|
||||||
reactStrictMode: false,
|
reactStrictMode: false,
|
||||||
experimental: {
|
experimental: {
|
||||||
webpackBuildWorker: true,
|
webpackBuildWorker: true
|
||||||
},
|
},
|
||||||
output: "standalone",
|
output: "standalone"
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = config;
|
module.exports = config;
|
||||||
|
|||||||
@@ -12,10 +12,11 @@ interface LinkFaviconProps {
|
|||||||
size?: number;
|
size?: number;
|
||||||
noMargin?: boolean;
|
noMargin?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function LinkFavicon({
|
export default function LinkFavicon({
|
||||||
url,
|
url,
|
||||||
size = 32,
|
size = 32,
|
||||||
noMargin = false,
|
noMargin = false
|
||||||
}: LinkFaviconProps) {
|
}: LinkFaviconProps) {
|
||||||
const [isFailed, setFailed] = useState<boolean>(false);
|
const [isFailed, setFailed] = useState<boolean>(false);
|
||||||
const [isLoading, setLoading] = useState<boolean>(true);
|
const [isLoading, setLoading] = useState<boolean>(true);
|
||||||
@@ -37,7 +38,7 @@ export default function LinkFavicon({
|
|||||||
setFallbackFavicon();
|
setFallbackFavicon();
|
||||||
handleStopLoading();
|
handleStopLoading();
|
||||||
}}
|
}}
|
||||||
onLoadingComplete={handleStopLoading}
|
onLoad={handleStopLoading}
|
||||||
height={size}
|
height={size}
|
||||||
width={size}
|
width={size}
|
||||||
alt="icon"
|
alt="icon"
|
||||||
|
|||||||
@@ -2,7 +2,14 @@ import { NextApiRequest, NextApiResponse } from "next";
|
|||||||
import { parse } from "node-html-parser";
|
import { parse } from "node-html-parser";
|
||||||
|
|
||||||
const USER_AGENT =
|
const USER_AGENT =
|
||||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36 Edg/108.0.1462.54";
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 Edg/119.0.0.0";
|
||||||
|
|
||||||
|
interface Favicon {
|
||||||
|
buffer: Buffer;
|
||||||
|
url: string;
|
||||||
|
type: string;
|
||||||
|
size: number;
|
||||||
|
}
|
||||||
|
|
||||||
export default async function handler(
|
export default async function handler(
|
||||||
req: NextApiRequest,
|
req: NextApiRequest,
|
||||||
@@ -10,71 +17,52 @@ export default async function handler(
|
|||||||
) {
|
) {
|
||||||
const urlParam = (req.query?.url as string) || "";
|
const urlParam = (req.query?.url as string) || "";
|
||||||
if (!urlParam) {
|
if (!urlParam) {
|
||||||
throw new Error("URL's missing");
|
throw new Error("URL is missing");
|
||||||
}
|
}
|
||||||
|
|
||||||
const urlRequest = urlParam + "/favicon.ico";
|
const { origin, href } = new URL(urlParam);
|
||||||
|
|
||||||
|
const faviconRequestUrl = origin + "/favicon.ico";
|
||||||
try {
|
try {
|
||||||
const { favicon, type, size } = await downloadImageFromUrl(
|
return sendImage(res, await getFavicon(faviconRequestUrl));
|
||||||
urlRequest + "/favicon.ico"
|
|
||||||
);
|
|
||||||
console.log("[Favicon]", `[first: ${urlRequest}]`, "request");
|
|
||||||
if (size === 0) {
|
|
||||||
throw new Error("Empty favicon");
|
|
||||||
}
|
|
||||||
if (!isImage(type)) {
|
|
||||||
throw new Error("Favicon path does not return an image");
|
|
||||||
}
|
|
||||||
return sendImage({
|
|
||||||
content: favicon,
|
|
||||||
res,
|
|
||||||
type,
|
|
||||||
size,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = error?.message || "Unable to retrieve favicon";
|
const errorMessage =
|
||||||
console.error("[Favicon]", `[first: ${urlRequest}]`, errorMessage);
|
error?.message || "Unable to retrieve favicon from favicon.ico url";
|
||||||
|
console.error("[Favicon]", `[first: ${faviconRequestUrl}]`, errorMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const requestDocument = await makeRequest(href);
|
||||||
|
const documentAsText = await requestDocument.text();
|
||||||
try {
|
try {
|
||||||
console.log("[Favicon]", `[second: ${urlRequest}]`, "new request");
|
const faviconPath = findFaviconPath(documentAsText);
|
||||||
const requestDocument = await makeRequest(urlRequest);
|
|
||||||
const text = await requestDocument.text();
|
|
||||||
|
|
||||||
const faviconPath = findFaviconPath(text);
|
|
||||||
if (!faviconPath) {
|
if (!faviconPath) {
|
||||||
throw new Error("Unable to find favicon path");
|
throw new Error("Unable to find favicon path");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isBase64Image(faviconPath)) {
|
if (isBase64Image(faviconPath)) {
|
||||||
console.log("base64, convert it to buffer");
|
console.log("[Favicon]", `[first: ${faviconRequestUrl}]`, "base64, convert it to buffer");
|
||||||
const buffer = convertBase64ToBuffer(faviconPath);
|
const buffer = convertBase64ToBuffer(faviconPath);
|
||||||
return sendImage({
|
return sendImage(res, {
|
||||||
content: buffer,
|
buffer,
|
||||||
res,
|
|
||||||
type: "image/x-icon",
|
type: "image/x-icon",
|
||||||
size: buffer.length,
|
size: buffer.length,
|
||||||
|
url: faviconPath
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const pathWithoutFile = popLastSegment(requestDocument.url);
|
const { href } = new URL(requestDocument.url);
|
||||||
const finalUrl = buildFaviconUrl(faviconPath, pathWithoutFile);
|
const finalUrl = href + faviconPath;
|
||||||
|
|
||||||
const { favicon, type, size } = await downloadImageFromUrl(finalUrl);
|
const favicon = await downloadImageFromUrl(finalUrl);
|
||||||
if (!isImage(type)) {
|
if (!isImage(favicon.type)) {
|
||||||
throw new Error("Favicon path does not return an image");
|
throw new Error("Favicon path does not return an image");
|
||||||
}
|
}
|
||||||
|
|
||||||
return sendImage({
|
return sendImage(res, favicon);
|
||||||
content: favicon,
|
|
||||||
res,
|
|
||||||
type,
|
|
||||||
size,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = error?.message || "Unable to retrieve favicon";
|
const errorMessage = error?.message || "Unable to retrieve favicon";
|
||||||
console.log("[Favicon]", `[second: ${urlRequest}]`, errorMessage);
|
console.log("[Favicon]", `[second: ${faviconRequestUrl}]`, errorMessage);
|
||||||
res.status(404).send({ error: errorMessage });
|
res.status(404).send({ error: errorMessage, text: documentAsText });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,92 +70,76 @@ async function makeRequest(url: string) {
|
|||||||
const headers = new Headers();
|
const headers = new Headers();
|
||||||
headers.set("User-Agent", USER_AGENT);
|
headers.set("User-Agent", USER_AGENT);
|
||||||
|
|
||||||
const request = await fetch(url, { headers, redirect: "follow" });
|
return await fetch(url, { headers, redirect: "follow" });
|
||||||
return request;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function downloadImageFromUrl(url: string): Promise<{
|
async function downloadImageFromUrl(url: string): Promise<Favicon> {
|
||||||
favicon: Buffer;
|
|
||||||
url: string;
|
|
||||||
type: string;
|
|
||||||
size: number;
|
|
||||||
}> {
|
|
||||||
const request = await makeRequest(url);
|
const request = await makeRequest(url);
|
||||||
const blob = await request.blob();
|
if (!request.ok) {
|
||||||
|
throw new Error("Request failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
const blob = await request.blob();
|
||||||
return {
|
return {
|
||||||
favicon: Buffer.from(await blob.arrayBuffer()),
|
buffer: Buffer.from(await blob.arrayBuffer()),
|
||||||
url: request.url,
|
url: request.url,
|
||||||
type: blob.type,
|
type: blob.type,
|
||||||
size: blob.size,
|
size: blob.size
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendImage({
|
function sendImage(res: NextApiResponse, {
|
||||||
content,
|
buffer,
|
||||||
res,
|
|
||||||
type,
|
type,
|
||||||
size,
|
size
|
||||||
}: {
|
}: Favicon) {
|
||||||
content: Buffer;
|
|
||||||
res: NextApiResponse;
|
|
||||||
type: string;
|
|
||||||
size: number;
|
|
||||||
}) {
|
|
||||||
res.setHeader("Content-Type", type);
|
res.setHeader("Content-Type", type);
|
||||||
res.setHeader("Content-Length", size);
|
res.setHeader("Content-Length", size);
|
||||||
res.send(content);
|
res.send(buffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectors = [
|
const rels = [
|
||||||
"link[rel='icon']",
|
"icon",
|
||||||
"link[rel='shortcut icon']",
|
"shortcut icon",
|
||||||
"link[rel='apple-touch-icon']",
|
"apple-touch-icon",
|
||||||
"link[rel='apple-touch-icon-precomposed']",
|
"apple-touch-icon-precomposed",
|
||||||
"link[rel='apple-touch-startup-image']",
|
"apple-touch-startup-image",
|
||||||
"link[rel='mask-icon']",
|
"mask-icon",
|
||||||
"link[rel='fluid-icon']",
|
"fluid-icon"
|
||||||
];
|
];
|
||||||
|
|
||||||
function findFaviconPath(text) {
|
function findFaviconPath(text: string) {
|
||||||
const document = parse(text);
|
const document = parse(text);
|
||||||
const links = document.querySelectorAll(selectors.join(", "));
|
const favicon = Array.from(document.getElementsByTagName("link")).find(
|
||||||
const link = links.find(
|
(element) => rels.includes(element.getAttribute("rel")) && element.getAttribute("href"));
|
||||||
(link) => !link.getAttribute("href").startsWith("data:image/")
|
|
||||||
);
|
if (!favicon) {
|
||||||
if (!link || !link.getAttribute("href")) {
|
|
||||||
throw new Error("No link/href attribute found");
|
throw new Error("No link/href attribute found");
|
||||||
}
|
}
|
||||||
|
|
||||||
return link.getAttribute("href");
|
return favicon.getAttribute("href");
|
||||||
}
|
|
||||||
|
|
||||||
function popLastSegment(url = "") {
|
|
||||||
const { href } = new URL(url);
|
|
||||||
const pathWithoutFile = href.split("/");
|
|
||||||
pathWithoutFile.pop();
|
|
||||||
return pathWithoutFile.join("/") || "";
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildFaviconUrl(faviconPath, pathWithoutFile) {
|
|
||||||
if (faviconPath.startsWith("http")) {
|
|
||||||
return faviconPath;
|
|
||||||
} else if (faviconPath.startsWith("/")) {
|
|
||||||
return pathWithoutFile + faviconPath;
|
|
||||||
} else {
|
|
||||||
return pathWithoutFile + "/" + faviconPath;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function isImage(type: string) {
|
function isImage(type: string) {
|
||||||
return type.includes("image");
|
return type.includes("image");
|
||||||
}
|
}
|
||||||
|
|
||||||
function isBase64Image(data) {
|
function isBase64Image(data: string) {
|
||||||
return data.startsWith("data:image/");
|
return data.startsWith("data:image/");
|
||||||
}
|
}
|
||||||
|
|
||||||
function convertBase64ToBuffer(base64 = ""): Buffer {
|
function convertBase64ToBuffer(base64: string): Buffer {
|
||||||
const buffer = Buffer.from(base64, "base64");
|
return Buffer.from(base64, "base64");
|
||||||
return buffer;
|
}
|
||||||
|
|
||||||
|
async function getFavicon(url: string): Promise<Favicon> {
|
||||||
|
if (!url) throw new Error("Missing URL");
|
||||||
|
|
||||||
|
const { origin } = new URL(url);
|
||||||
|
const favicon = await downloadImageFromUrl(url);
|
||||||
|
if (!isImage(favicon.type) || favicon.size === 0) {
|
||||||
|
throw new Error("Favicon path does not return an image");
|
||||||
|
}
|
||||||
|
|
||||||
|
return favicon;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,10 +15,10 @@ import { useMediaQuery } from "hooks/useMediaQuery";
|
|||||||
import useModal from "hooks/useModal";
|
import useModal from "hooks/useModal";
|
||||||
import { getServerSideTranslation } from "i18n";
|
import { getServerSideTranslation } from "i18n";
|
||||||
import getUserCategories from "lib/category/getUserCategories";
|
import getUserCategories from "lib/category/getUserCategories";
|
||||||
|
import { useTranslation } from "next-i18next";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useCallback, useMemo, useState } from "react";
|
import { useCallback, useMemo, useState } from "react";
|
||||||
import { useHotkeys } from "react-hotkeys-hook";
|
import { useHotkeys } from "react-hotkeys-hook";
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { Category, Link, SearchItem } from "types";
|
import { Category, Link, SearchItem } from "types";
|
||||||
import { withAuthentication } from "utils/session";
|
import { withAuthentication } from "utils/session";
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user