Skip to content

Commit 5b8026e

Browse files
committed
refactor: separate fetch-favicon into its own lib fn
1 parent 8a57da1 commit 5b8026e

File tree

2 files changed

+63
-22
lines changed

2 files changed

+63
-22
lines changed

src/lib/fetch-favicon.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { load as cheerioLoad } from "cheerio";
2+
3+
/**
4+
@param siteBaseURL - it should only contain base url of the site
5+
6+
TODO: what about GH Pages? do they work?
7+
*/
8+
export async function fetchFavicon(
9+
siteBaseURL: string,
10+
): Promise<string | undefined> {
11+
const websiteRes = await fetch(siteBaseURL);
12+
if (!websiteRes.ok) {
13+
return undefined;
14+
}
15+
16+
// fetch link[rel="icon"].href
17+
{
18+
const html = await websiteRes.text();
19+
const querySelector = cheerioLoad(html);
20+
const iconHref = querySelector(
21+
"link[rel='icon'], link[rel='shortcut icon']",
22+
)[0]?.attribs.href;
23+
if (iconHref) {
24+
const iconUrl = new URL(iconHref, siteBaseURL);
25+
const iconRes = await fetch(iconUrl);
26+
const contentType = iconRes.headers.get("content-type");
27+
if (!contentType)
28+
console.warn(
29+
`header "Content-Type" not found while fetching ${iconUrl.toString()}`,
30+
);
31+
if (iconRes.ok) {
32+
return base64DataURL(
33+
contentType,
34+
Buffer.from(await iconRes.arrayBuffer()),
35+
);
36+
}
37+
}
38+
}
39+
40+
// fetch /favicon.ico
41+
{
42+
const faviconRes = await fetch(
43+
`${new URL(siteBaseURL).origin}/favicon.ico`,
44+
);
45+
const contentType =
46+
faviconRes.headers.get("content-type") ?? "image/vnd.microsoft.icon";
47+
if (faviconRes.ok && !contentType.startsWith("text/")) {
48+
return base64DataURL(
49+
contentType,
50+
Buffer.from(await faviconRes.arrayBuffer()),
51+
);
52+
}
53+
}
54+
55+
return undefined;
56+
}
57+
58+
function base64DataURL(contentType: string | null, buf: Buffer) {
59+
const dataType = contentType ? `${contentType};base64` : "base64";
60+
return `data:${dataType},${buf.toString("base64")}`;
61+
}

src/pages/projects/[...id].astro

Lines changed: 2 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ import { render } from "astro:content";
44
import JoinUsCTA from "+/components/common/JoinUsCTA.astro";
55
import ActionButton from "+/components/utils/ActionButton.astro";
66
import GlobalLayout from "+/layouts/GlobalLayout.astro";
7+
import { fetchFavicon } from "+/lib/fetch-favicon";
78
import { getProjects } from "+/query";
89
import { Focus } from "+/schema.ts";
910
import type { GetStaticPaths } from "astro";
1011
import { Icon } from "astro-icon/components";
11-
import { load as cheerioLoad } from "cheerio";
1212
1313
export const getStaticPaths = (async () => {
1414
const projects = await getProjects("all");
@@ -22,27 +22,7 @@ const { Content } = await render(project);
2222
2323
let iconSrc: string | undefined = undefined;
2424
if (project.data.website && project.data.status !== "dead") {
25-
const websiteRes = await fetch(project.data.website);
26-
if (websiteRes.ok) {
27-
const websiteHtml = await websiteRes.text();
28-
const website = cheerioLoad(websiteHtml);
29-
const iconHref = website("link[rel='icon'], link[rel='shortcut icon']")[0]
30-
?.attribs.href;
31-
if (iconHref) {
32-
const faviconRes = await fetch(new URL(iconHref, project.data.website));
33-
iconSrc = `data:${faviconRes.headers.get("content-type")};base64,${Buffer.from(await faviconRes.arrayBuffer()).toString("base64")}`;
34-
} else {
35-
const faviconRes = await fetch(
36-
`${new URL(project.data.website).origin}/favicon.ico`,
37-
);
38-
if (
39-
faviconRes.ok &&
40-
!faviconRes.headers.get("content-type")?.startsWith("text/")
41-
) {
42-
iconSrc = `data:${faviconRes.headers.get("content-type")};base64,${Buffer.from(await faviconRes.arrayBuffer()).toString("base64")}`;
43-
}
44-
}
45-
}
25+
iconSrc = await fetchFavicon(project.data.website);
4626
}
4727
---
4828

0 commit comments

Comments
 (0)