add default iframe icon, optimize iframe fetch behavior

This commit is contained in:
James Fenn
2023-02-12 18:29:27 -05:00
parent 9bc8da4278
commit f1cc8b3a98
4 changed files with 154 additions and 114 deletions

BIN
public/link.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -0,0 +1,19 @@
import * as astroImage from "@astrojs/image";
import {
GetPictureParams,
GetPictureResult,
} from "@astrojs/image/dist/lib/get-picture";
import sharp_service from "../../../node_modules/@astrojs/image/dist/loaders/sharp.js";
export function getPicture(
params: GetPictureParams
): Promise<GetPictureResult> {
// HACK: This is a hack that heavily relies on `getImage`'s internals :(
globalThis.astroImage = {
...(globalThis.astroImage || {}),
loader: globalThis.astroImage?.loader ?? sharp_service,
defaultLoader: globalThis.astroImage?.defaultLoader ?? sharp_service,
};
return astroImage.getPicture(params);
}

View File

@@ -9,8 +9,7 @@ import path from "path";
/**
* They need to be the same `getImage` with the same `globalThis` instance, thanks to the "hack" workaround.
*/
import { getPicture } from "../../../node_modules/@astrojs/image/dist/index.js";
import sharp_service from "../../../node_modules/@astrojs/image/dist/loaders/sharp.js";
import { getPicture } from "./get-picture-hack";
import { getImageSize } from "../get-image-size";
import { fileURLToPath } from "url";
import { getFullRelativePath } from "../url-paths";
@@ -28,13 +27,6 @@ export const rehypeAstroImageMd: Plugin<
Root
> = ({ maxHeight, maxWidth }) => {
return async (tree, file) => {
// HACK: This is a hack that heavily relies on `getImage`'s internals :(
globalThis.astroImage = {
...(globalThis.astroImage || {}),
loader: sharp_service ?? globalThis.astroImage?.loader,
defaultLoader: sharp_service ?? globalThis.astroImage?.defaultLoader,
};
const imgNodes: any[] = [];
visit(tree, (node: any) => {
if (node.tagName === "img") {

View File

@@ -7,37 +7,140 @@ import { EMBED_SIZE } from "./constants";
import { h } from "hastscript";
import { fromHtml } from "hast-util-from-html";
import find from "unist-util-find";
import { getLargestManifestIcon, Manifest } from "../get-largest-manifest-icon";
/**
* They need to be the same `getImage` with the same `globalThis` instance, thanks to the "hack" workaround.
*/
import { getPicture } from "../../../node_modules/@astrojs/image/dist/index.js";
import sharp_service from "../../../node_modules/@astrojs/image/dist/loaders/sharp.js";
import { getLargestManifestIcon } from "../get-largest-manifest-icon";
import { getPicture } from "./get-picture-hack";
import type { GetPictureResult } from "@astrojs/image/dist/lib/get-picture";
// This does not download the whole file to get the file size
import probe from "probe-image-size";
interface RehypeUnicornIFrameClickToRunProps {}
const ManifestIconMap = new Map<
string,
{ result: GetPictureResult; height: number; width: number }
>();
// default icon, used if a frame's favicon cannot be resolved
let defaultPageIcon: GetPictureResult | null;
async function fetchDefaultPageIcon() {
return (
defaultPageIcon ||
(defaultPageIcon = await getPicture({
src: "/link.png",
widths: [50],
formats: ["avif", "webp", "png"],
aspectRatio: 1,
alt: "",
}))
);
}
// Cache the fetch *promises* - so that only one request per manifest/icon is processed,
// and multiple fetchPageInfo() calls can await the same icon
const pageIconMap = new Map<string, Promise<GetPictureResult>>();
function fetchPageIcon(src: URL, srcHast: Root): Promise<GetPictureResult> {
if (pageIconMap.has(src.origin)) return pageIconMap.get(src.origin);
const promise = (async () => {
// <link rel="manifest" href="/manifest.json">
const manifestPath = find(
srcHast,
(node) => node?.properties?.rel?.[0] === "manifest"
);
let iconLink: string;
if (manifestPath) {
// `/manifest.json`
const manifestRelativeURL = manifestPath.properties.href;
const fullManifestURL = new URL(manifestRelativeURL, src).href;
const manifest = await fetch(fullManifestURL)
.then((r) => r.status === 200 && r.json())
.catch(() => null);
if (manifest) {
const largestIcon = getLargestManifestIcon(manifest);
iconLink = new URL(largestIcon.icon.src, src.origin).href;
}
}
if (!iconLink) {
// fetch `favicon.ico`
// <link rel="shortcut icon" type="image/png" href="https://example.com/img.png">
const favicon = find(srcHast, (node) =>
node?.properties?.rel?.includes("icon")
);
if (favicon) {
iconLink = new URL(favicon.properties.href, src).href;
}
}
// no icon image URL is found
if (!iconLink) return null;
const { height: imgHeight, width: imgWidth } = await probe(iconLink);
const aspectRatio = imgHeight / imgWidth;
return await getPicture({
src: iconLink,
widths: [50],
formats: ["avif", "webp", "png"],
aspectRatio: aspectRatio,
alt: "",
});
})()
// if an error is thrown, or response is null, use the default page icon
.catch(() => null)
.then((p) => p || fetchDefaultPageIcon());
pageIconMap.set(src.origin, promise);
return promise;
}
const pageHtmlMap = new Map<string, Promise<Root | null>>();
function fetchPageHtml(src: string): Promise<Root | null> {
if (pageHtmlMap.has(src)) return pageHtmlMap.get(src);
const promise = (async () => {
const srcHTML = await fetch(src)
.then((r) => r.status === 200 && r.text())
.catch(() => null);
// if fetch fails...
if (!srcHTML) return null;
const srcHast = fromHtml(srcHTML);
return srcHast;
})();
pageHtmlMap.set(src, promise);
return promise;
}
type PageInfo = {
title?: string;
icon: GetPictureResult;
};
async function fetchPageInfo(src: string): Promise<PageInfo | null> {
// fetch origin url, catch any connection timeout errors
const url = new URL(src);
url.search = ""; // remove any search params
const srcHast = await fetchPageHtml(url.toString());
if (!srcHast) return null;
// find <title> element in response HTML
const titleEl = find(srcHast, { tagName: "title" });
const title = titleEl ? titleEl.children[0].value : undefined;
// find the page favicon (cache by page origin)
const icon = await fetchPageIcon(url, srcHast);
return { title, icon };
}
// TODO: Add switch/case and dedicated files ala "Components"
export const rehypeUnicornIFrameClickToRun: Plugin<
[RehypeUnicornIFrameClickToRunProps | never],
Root
> = () => {
// HACK: This is a hack that heavily relies on `getImage`'s internals :(
globalThis.astroImage = {
...(globalThis.astroImage || {}),
loader: sharp_service ?? globalThis.astroImage?.loader,
defaultLoader: sharp_service ?? globalThis.astroImage?.defaultLoader,
};
return async (tree, file) => {
return async (tree) => {
const iframeNodes: any[] = [];
visit(tree, (node: any) => {
if (node.tagName === "iframe") {
@@ -49,84 +152,11 @@ export const rehypeUnicornIFrameClickToRun: Plugin<
iframeNodes.map(async (iframeNode) => {
const width = iframeNode.properties.width ?? EMBED_SIZE.w;
const height = iframeNode.properties.height ?? EMBED_SIZE.h;
const req = await fetch(iframeNode.properties.src).catch(() => null);
let pageTitleString: string | undefined;
let iconLink: string | undefined;
let iframePicture:
| ReturnType<(typeof ManifestIconMap)["get"]>
| undefined;
const iframeOrigin = new URL(iframeNode.properties.src).origin;
if (req && req.status === 200) {
const srcHTML = await req.text();
const srcHast = fromHtml(srcHTML);
const titleEl = find(srcHast, { tagName: "title" });
pageTitleString = titleEl.children[0].value;
if (ManifestIconMap.has(iframeOrigin)) {
iframePicture = ManifestIconMap.get(iframeOrigin);
} else {
// <link rel="manifest" href="/manifest.json">
const manifestPath = find(
srcHast,
(node) => node?.properties?.rel?.[0] === "manifest"
);
if (manifestPath) {
// `/manifest.json`
const manifestRelativeURL = manifestPath.properties.href;
const fullManifestURL = new URL(
manifestRelativeURL,
const info: PageInfo = (await fetchPageInfo(
iframeNode.properties.src
).href;
const manifestReq = await fetch(fullManifestURL).catch(
() => null
);
if (manifestReq && manifestReq.status === 200) {
const manifestContents = await manifestReq.text();
const manifestJSON: Manifest = JSON.parse(manifestContents);
const largestIcon = getLargestManifestIcon(manifestJSON);
iconLink = new URL(largestIcon.icon.src, iframeOrigin).href;
}
} else {
// fetch `favicon.ico`
// <link rel="icon" type="image/png" href="https://example.com/img.png">
const favicon = find(
srcHast,
(node) => node?.properties?.rel?.[0] === "icon"
);
if (favicon) {
iconLink = new URL(favicon.properties.href, iframeOrigin).href;
}
}
}
}
).catch(() => null)) || { icon: await fetchDefaultPageIcon() };
if (iconLink) {
try {
const { height: imgHeight, width: imgWidth } = await probe(
iconLink
);
const aspectRatio = imgHeight / imgWidth;
const result = await getPicture({
src: iconLink,
widths: [50],
formats: ["webp", "png"],
aspectRatio: aspectRatio,
alt: "",
});
iframePicture = { result, height: imgHeight, width: imgWidth };
ManifestIconMap.set(iframeOrigin, iframePicture);
// eslint-disable-next-line no-empty
} catch (_e) {}
}
// TODO: Add placeholder image
const sources =
iframePicture &&
iframePicture.result.sources.map((attrs) => {
const sources = info.icon.sources.map((attrs) => {
return h("source", attrs);
});
@@ -135,30 +165,29 @@ export const rehypeUnicornIFrameClickToRun: Plugin<
{
class: "iframe-replacement-container",
"data-iframeurl": iframeNode.properties.src,
"data-pagetitle": pageTitleString,
"data-pageicon": iframePicture
? JSON.stringify(iframePicture)
: undefined,
"data-pagetitle": info.title,
"data-pageicon": info.icon ? JSON.stringify(info.icon) : undefined,
style: `height: ${
Number(height) ? `${height}px` : height
}; width: ${Number(width) ? `${width}px` : width};`,
},
[
iframePicture
info.icon
? h("picture", [
...sources,
h("img", {
...(iframePicture.result.image as never as object),
...(info.icon.image as object),
class: "iframe-replacement-icon",
alt: "",
loading: "lazy",
decoding: "async",
"data-nozoom": "true",
}),
])
: null,
h("p", { class: "iframe-replacement-title" }, [
h("span", { class: "visually-hidden" }, ["An embedded webpage:"]),
pageTitleString,
info.title,
]),
h("button", { class: "baseBtn iframe-replacement-button" }, [
"Run embed",