@realfavicongenerator/check-favicon
Version:
Check the favicon of a website
235 lines (212 loc) • 8.03 kB
text/typescript
import { CheckerMessage, CheckerStatus, DesktopFaviconReport, DesktopSingleReport, Fetcher, MessageId } from "../types";
import { HTMLElement } from 'node-html-parser'
import sharp from 'sharp'
import { CheckIconProcessor, bufferToDataUrl, checkIcon, fetchFetcher, mergeUrlAndPath, readableStreamToString } from "../helper";
import { checkIcoFavicon } from "./ico";
export const PngFaviconFileSize = 96;
export const checkSvgFavicon = async (baseUrl: string, head: HTMLElement | null, fetcher: Fetcher = fetchFetcher): Promise<DesktopSingleReport> => {
const messages: CheckerMessage[] = [];
if (!head) {
messages.push({
status: CheckerStatus.Error,
id: MessageId.noHead,
text: 'No <head> element'
});
return {
messages,
icon: { content: null, url: null, width: null, height: null }
};
}
const svgs = head?.querySelectorAll("link[rel='icon'][type='image/svg+xml']");
if (svgs.length === 0) {
messages.push({
status: CheckerStatus.Error,
id: MessageId.noSvgFavicon,
text: 'There is no SVG favicon'
});
} else if (svgs.length > 1) {
messages.push({
status: CheckerStatus.Error,
id: MessageId.multipleSvgFavicons,
text: `There are ${svgs.length} SVG favicons`
});
} else {
messages.push({
status: CheckerStatus.Ok,
id: MessageId.svgFaviconDeclared,
text: 'The SVG favicon is declared'
});
const href = svgs[0].attributes.href;
if (!href) {
messages.push({
status: CheckerStatus.Error,
id: MessageId.noSvgFaviconHref,
text: 'The SVG markup has no href attribute'
});
} else {
const iconReport = await checkSvgFaviconFile(baseUrl, href, fetcher)
return {
messages: [ ...messages, ...iconReport.messages ],
icon: iconReport.icon
};
}
}
return {
messages,
icon: { content: null, url: null, width: null, height: null }
};
}
export const checkSvgFaviconFile = async (baseUrl: string, url: string, fetcher: Fetcher): Promise<DesktopSingleReport> => {
const messages: CheckerMessage[] = [];
const svgUrl = mergeUrlAndPath(baseUrl, url);
const res = await fetcher(svgUrl, 'image/svg+xml');
let content;
let width: number | null = null;
let height: number | null = null;
if (res.status === 404) {
messages.push({
status: CheckerStatus.Error,
id: MessageId.svgFavicon404,
text: `The SVG icon file \`${url}\` does not exist (404 error)`
});
} else if (res.status >= 300) {
messages.push({
status: CheckerStatus.Error,
id: MessageId.svgFaviconCannotGet,
text: `Cannot get the SVG icon file at \`${url}\` (${res.status} error)`
});
} else if (res.readableStream) {
messages.push({
status: CheckerStatus.Ok,
id: MessageId.svgFaviconDownloadable,
text: `The SVG favicon is accessible at \`${url}\``
});
content = await readableStreamToString(res.readableStream);
const meta = await sharp(Buffer.from(content)).metadata();
width = meta.width || null;
height = meta.height || null;
if (width && height && width !== height) {
messages.push({
status: CheckerStatus.Error,
id: MessageId.svgFaviconNotSquare,
text: `The SVG is not square (${width}x${height})`
});
} else {
messages.push({
status: CheckerStatus.Ok,
id: MessageId.svgFaviconSquare,
text: `The SVG is square (${width}x${height})`
});
}
}
return {
messages,
icon: {
content: content ? await bufferToDataUrl(Buffer.from(content), 'image/svg+xml') : null,
url: svgUrl,
width, height
}
};
}
export const checkPngFavicon = async (baseUrl: string, head: HTMLElement | null, fetcher: Fetcher = fetchFetcher): Promise<DesktopSingleReport> => {
const messages: CheckerMessage[] = [];
if (!head) {
messages.push({
status: CheckerStatus.Error,
id: MessageId.noHead,
text: 'No <head> element'
});
return { messages, icon: { content: null, url: null, width: null, height: null } };
}
const icons = head?.querySelectorAll("link[rel='icon'][type='image/png']");
if (icons.length === 0) {
messages.push({
status: CheckerStatus.Error,
id: MessageId.noDesktopPngFavicon,
text: 'There is no desktop PNG favicon'
});
} else {
const size = `${PngFaviconFileSize}x${PngFaviconFileSize}`;
const sizedIconMarkup = icons.filter(icon => icon.attributes.sizes === size);
if (sizedIconMarkup.length === 0) {
messages.push({
status: CheckerStatus.Error,
id: MessageId.no96x96DesktopPngFavicon,
text: `There is no ${size} desktop PNG favicon`
});
} else {
messages.push({
status: CheckerStatus.Ok,
id: MessageId.desktopPngFaviconDeclared,
text: `The ${size} desktop PNG favicon is declared`
});
const href = sizedIconMarkup[0].attributes.href;
if (!href) {
messages.push({
status: CheckerStatus.Error,
id: MessageId.noDesktopPngFaviconHref,
text: `The ${size} desktop favicon markup has no href attribute`
});
} else {
const iconUrl = mergeUrlAndPath(baseUrl, href);
const processor: CheckIconProcessor = {
cannotGet: (httpStatus) => {
messages.push({
status: CheckerStatus.Error,
id: MessageId.desktopPngFaviconCannotGet,
text: `Cannot get the ${size} desktop PNG favicon at \`${iconUrl}\` (${httpStatus} error)`
});
},
downloadable: () => {
messages.push({
status: CheckerStatus.Ok,
id: MessageId.desktopPngFaviconDownloadable,
text: `The ${size} desktop PNG favicon is accessible`
});
},
icon404: () => {
messages.push({
status: CheckerStatus.Error,
id: MessageId.desktopPngFavicon404,
text: `The ${size} desktop PNG favicon does not exist (404 error)`
});
},
notSquare: (width, height) => {}, // Ignore this message
wrongSize: (widthHeight) => {
messages.push({
status: CheckerStatus.Error,
id: MessageId.desktopPngFaviconWrongSize,
text: `The ${size} desktop PNG favicon has the wrong size (${widthHeight}x${widthHeight})`
});
},
rightSize(widthHeight) {
messages.push({
status: CheckerStatus.Ok,
id: MessageId.desktopPngFaviconRightSize,
text: `The ${size} desktop PNG favicon has the right size`
});
},
square: (widthHeight) => {}, // Ignore this message,
noHref: () => {} // Ignore this message
};
const icon = await checkIcon(iconUrl, processor, fetcher, icons[0].attributes.mimeType || 'image/png', PngFaviconFileSize);
return { messages, icon }
}
}
}
return { messages, icon: { content: null, url: null, width: null, height: null } };
}
export const checkDesktopFavicon = async (baseUrl: string, head: HTMLElement | null, fetcher: Fetcher = fetchFetcher): Promise<DesktopFaviconReport> => {
const svgReport = await checkSvgFavicon(baseUrl, head, fetcher);
const pngReport = await checkPngFavicon(baseUrl, head, fetcher);
const icoReport = await checkIcoFavicon(baseUrl, head, fetcher);
return {
messages: [ ...svgReport.messages, ...pngReport.messages, ...icoReport.messages ],
icon: pngReport.icon ? pngReport.icon.content : null,
icons: {
png: pngReport.icon,
ico: icoReport.icon,
svg: svgReport.icon
}
};
}