From c35eb678901e451fcc2b178d1717509ae0201b0e Mon Sep 17 00:00:00 2001 From: Sonny Date: Sun, 12 Nov 2023 01:44:30 +0100 Subject: [PATCH] fix: some cases where favicon are broken --- .gitignore | 2 + next.config.js | 16 ++- src/components/Links/LinkFavicon.tsx | 5 +- src/pages/api/favicon.ts | 172 +++++++++++---------------- src/pages/index.tsx | 2 +- 5 files changed, 88 insertions(+), 109 deletions(-) diff --git a/.gitignore b/.gitignore index 8ba76d6..1e96ef9 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,8 @@ # misc .DS_Store *.pem +.idea +.vscode # debug npm-debug.log* diff --git a/next.config.js b/next.config.js index 0628de1..fe0dce2 100644 --- a/next.config.js +++ b/next.config.js @@ -1,25 +1,29 @@ const { i18n } = require("./next-i18next.config"); -/** @type {import('next').NextConfig} */ +/** @type {import("next").NextConfig} */ const config = { i18n, webpack(config) { config.module.rules.push({ test: /\.svg$/, - use: ["@svgr/webpack"], + use: ["@svgr/webpack"] }); return config; }, images: { - domains: ["localhost", "t3.gstatic.com", "lh3.googleusercontent.com"], - formats: ["image/webp"], + remotePatterns: [ + { hostname: "localhost" }, + { hostname: "t3.gstatic.com" }, + { hostname: "lh3.googleusercontent.com" } + ], + formats: ["image/webp"] }, reactStrictMode: false, experimental: { - webpackBuildWorker: true, + webpackBuildWorker: true }, - output: "standalone", + output: "standalone" }; module.exports = config; diff --git a/src/components/Links/LinkFavicon.tsx b/src/components/Links/LinkFavicon.tsx index 5520b92..073581c 100644 --- a/src/components/Links/LinkFavicon.tsx +++ b/src/components/Links/LinkFavicon.tsx @@ -12,10 +12,11 @@ interface LinkFaviconProps { size?: number; noMargin?: boolean; } + export default function LinkFavicon({ url, size = 32, - noMargin = false, + noMargin = false }: LinkFaviconProps) { const [isFailed, setFailed] = useState(false); const [isLoading, setLoading] = useState(true); @@ -37,7 +38,7 @@ export default function LinkFavicon({ setFallbackFavicon(); handleStopLoading(); }} - onLoadingComplete={handleStopLoading} + onLoad={handleStopLoading} height={size} width={size} alt="icon" diff --git a/src/pages/api/favicon.ts b/src/pages/api/favicon.ts index 80a6702..3cdc88c 100644 --- a/src/pages/api/favicon.ts +++ b/src/pages/api/favicon.ts @@ -2,7 +2,14 @@ import { NextApiRequest, NextApiResponse } from "next"; import { parse } from "node-html-parser"; 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( req: NextApiRequest, @@ -10,71 +17,52 @@ export default async function handler( ) { const urlParam = (req.query?.url as string) || ""; 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 { - const { favicon, type, size } = await downloadImageFromUrl( - 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, - }); + return sendImage(res, await getFavicon(faviconRequestUrl)); } catch (error) { - const errorMessage = error?.message || "Unable to retrieve favicon"; - console.error("[Favicon]", `[first: ${urlRequest}]`, errorMessage); + const 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 { - console.log("[Favicon]", `[second: ${urlRequest}]`, "new request"); - const requestDocument = await makeRequest(urlRequest); - const text = await requestDocument.text(); - - const faviconPath = findFaviconPath(text); + const faviconPath = findFaviconPath(documentAsText); if (!faviconPath) { throw new Error("Unable to find favicon path"); } if (isBase64Image(faviconPath)) { - console.log("base64, convert it to buffer"); + console.log("[Favicon]", `[first: ${faviconRequestUrl}]`, "base64, convert it to buffer"); const buffer = convertBase64ToBuffer(faviconPath); - return sendImage({ - content: buffer, - res, + return sendImage(res, { + buffer, type: "image/x-icon", size: buffer.length, + url: faviconPath }); } - const pathWithoutFile = popLastSegment(requestDocument.url); - const finalUrl = buildFaviconUrl(faviconPath, pathWithoutFile); + const { href } = new URL(requestDocument.url); + const finalUrl = href + faviconPath; - const { favicon, type, size } = await downloadImageFromUrl(finalUrl); - if (!isImage(type)) { + const favicon = await downloadImageFromUrl(finalUrl); + if (!isImage(favicon.type)) { throw new Error("Favicon path does not return an image"); } - return sendImage({ - content: favicon, - res, - type, - size, - }); + return sendImage(res, favicon); } catch (error) { const errorMessage = error?.message || "Unable to retrieve favicon"; - console.log("[Favicon]", `[second: ${urlRequest}]`, errorMessage); - res.status(404).send({ error: errorMessage }); + console.log("[Favicon]", `[second: ${faviconRequestUrl}]`, errorMessage); + res.status(404).send({ error: errorMessage, text: documentAsText }); } } @@ -82,92 +70,76 @@ async function makeRequest(url: string) { const headers = new Headers(); headers.set("User-Agent", USER_AGENT); - const request = await fetch(url, { headers, redirect: "follow" }); - return request; + return await fetch(url, { headers, redirect: "follow" }); } -async function downloadImageFromUrl(url: string): Promise<{ - favicon: Buffer; - url: string; - type: string; - size: number; -}> { +async function downloadImageFromUrl(url: string): Promise { const request = await makeRequest(url); - const blob = await request.blob(); + if (!request.ok) { + throw new Error("Request failed"); + } + const blob = await request.blob(); return { - favicon: Buffer.from(await blob.arrayBuffer()), + buffer: Buffer.from(await blob.arrayBuffer()), url: request.url, type: blob.type, - size: blob.size, + size: blob.size }; } -function sendImage({ - content, - res, +function sendImage(res: NextApiResponse, { + buffer, type, - size, -}: { - content: Buffer; - res: NextApiResponse; - type: string; - size: number; -}) { + size +}: Favicon) { res.setHeader("Content-Type", type); res.setHeader("Content-Length", size); - res.send(content); + res.send(buffer); } -const selectors = [ - "link[rel='icon']", - "link[rel='shortcut icon']", - "link[rel='apple-touch-icon']", - "link[rel='apple-touch-icon-precomposed']", - "link[rel='apple-touch-startup-image']", - "link[rel='mask-icon']", - "link[rel='fluid-icon']", +const rels = [ + "icon", + "shortcut icon", + "apple-touch-icon", + "apple-touch-icon-precomposed", + "apple-touch-startup-image", + "mask-icon", + "fluid-icon" ]; -function findFaviconPath(text) { +function findFaviconPath(text: string) { const document = parse(text); - const links = document.querySelectorAll(selectors.join(", ")); - const link = links.find( - (link) => !link.getAttribute("href").startsWith("data:image/") - ); - if (!link || !link.getAttribute("href")) { + const favicon = Array.from(document.getElementsByTagName("link")).find( + (element) => rels.includes(element.getAttribute("rel")) && element.getAttribute("href")); + + if (!favicon) { throw new Error("No link/href attribute found"); } - return link.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; - } + return favicon.getAttribute("href"); } function isImage(type: string) { return type.includes("image"); } -function isBase64Image(data) { +function isBase64Image(data: string) { return data.startsWith("data:image/"); } -function convertBase64ToBuffer(base64 = ""): Buffer { - const buffer = Buffer.from(base64, "base64"); - return buffer; +function convertBase64ToBuffer(base64: string): Buffer { + return Buffer.from(base64, "base64"); +} + +async function getFavicon(url: string): Promise { + 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; } diff --git a/src/pages/index.tsx b/src/pages/index.tsx index b8a65dc..72fae7b 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -15,10 +15,10 @@ import { useMediaQuery } from "hooks/useMediaQuery"; import useModal from "hooks/useModal"; import { getServerSideTranslation } from "i18n"; import getUserCategories from "lib/category/getUserCategories"; +import { useTranslation } from "next-i18next"; import { useRouter } from "next/router"; import { useCallback, useMemo, useState } from "react"; import { useHotkeys } from "react-hotkeys-hook"; -import { useTranslation } from "react-i18next"; import { Category, Link, SearchItem } from "types"; import { withAuthentication } from "utils/session";