processImage = async (img, outputPath) => {
  let src = img.getAttribute("src");
  if (/^(https?\:\/\/|\/\/)/i.test(src)) {
    return;
  }
  if (/^\.+\//.test(src)) {
    // resolve relative URL
    src =
      "/" +
      path.relative("./_site/", path.resolve(path.dirname(outputPath), src));
    if (path.sep == "\\") {
      src = src.replace(/\\/g, "/");
    }
  }
  let dimensions;
  try {
    dimensions = await sizeOf("_site/" + src);
  } catch (e) {
    console.warn(e.message, src);
    return;
  }
  if (!img.getAttribute("width")) {
    img.setAttribute("width", dimensions.width);
    img.setAttribute("height", dimensions.height);
  }
  const inputType = dimensions.type;
  if (inputType == "svg") {
    return;
  }
  if (inputType == "gif") {
    const videoSrc = await gif2mp4(src);
    const video = img.ownerDocument.createElement(
      /AMP/i.test(img.tagName) ? "amp-video" : "video"
    );
    [...img.attributes].map(({ name, value }) => {
      video.setAttribute(name, value);
    });
    video.src = videoSrc;
    video.setAttribute("autoplay", "");
    video.setAttribute("muted", "");
    video.setAttribute("loop", "");
    if (!video.getAttribute("aria-label")) {
      video.setAttribute("aria-label", img.getAttribute("alt"));
      video.removeAttribute("alt");
    }
    img.parentElement.replaceChild(video, img);
    return;
  }
  // When the input is a PNG, we keep the fallback image a PNG because JPEG does
  // not support transparency. However, we still optimize to AVIF/WEBP in a lossy
  // fashion. It may be worth adding a feature to opt-out of lossy optimization.
  const fallbackType = inputType == "png" ? "png" : "jpeg";
  if (img.tagName == "IMG") {
    img.setAttribute("decoding", "async");
    img.setAttribute("loading", "lazy");
    img.setAttribute(
      "style",
      `background-size:cover;` +
        `background-image:url("${await blurryPlaceholder(src)}")`
    );
    const doc = img.ownerDocument;
    const picture = doc.createElement("picture");
    const avif = doc.createElement("source");
    const webp = doc.createElement("source");
    const jpeg = doc.createElement("source");
    await setSrcset(avif, src, "avif");
    avif.setAttribute("type", "image/avif");
    await setSrcset(webp, src, "webp");
    webp.setAttribute("type", "image/webp");
    const fallback = await setSrcset(jpeg, src, fallbackType);
    jpeg.setAttribute("type", `image/${fallbackType}`);
    picture.appendChild(avif);
    picture.appendChild(webp);
    picture.appendChild(jpeg);
    img.parentElement.replaceChild(picture, img);
    picture.appendChild(img);
    img.setAttribute("src", fallback);
  } else if (!img.getAttribute("srcset")) {
    const fallback = await setSrcset(img, src, fallbackType);
    img.setAttribute("src", fallback);
  }
}
Sets `width` and `height` on each image, adds blurry placeholder
and generates a srcset if none present.
Note, that the static `sizes` string would need to change for a different
blog layout.