eleventy-chirpy-blog-template/_11ty/srcset.js
2025-06-25 14:09:10 +02:00

163 lines
5.0 KiB
JavaScript

/**
* Copyright (c) 2020 Google Inc
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
// Transformer to set `srcset` for all images, and to create respective
// image versions in different formats and resolutions.
// Modification of original script from https://github.com/google/eleventy-high-performance-blog
const { JSDOM } = require("jsdom");
const sharp = require("sharp");
const { copyFileSync, existsSync, mkdirSync, readFileSync } = require("fs");
const { MD5 } = require("crypto-js");
const { extname, join } = require("path");
const widths = [1024, 820, 640, 320];
const extension = {
jpeg: "jpg",
webp: "webp"
};
// Map filenames to types and width, and then resize
async function srcset(filename, hash, format, metadataWidth) {
// Create a map of all file names
const names = await Promise.all(
widths.map((width) =>
resize(filename, width, hash, format, metadataWidth)
)
);
return names.map((n, i) => `${n} ${widths[i]}w`).join(", ");
}
async function resize(filename, width, hash, format, metadataWidth) {
const out = sizedName(filename, width, hash, format);
if (existsSync("_site/" + out)) {
return out;
}
const file = join(process.cwd(), filename);
const resizeWidth = metadataWidth < width ? metadataWidth : width;
await sharp(file)
.resize({ width: resizeWidth })
[format]({
quality: 80,
reductionEffort: 6
})
.toFile("_site/" + out);
return out;
}
function sizedName(filename, width, hash, format) {
const ext = extension[format];
if (!ext) {
throw new Error(`Unknown format ${format}`);
}
return filename.replace(
/\.\w+$/,
() => "-" + hash + "-" + width + "w." + ext
);
}
function hashedName(filename, hash) {
return filename.replace(extname(filename), "-" + hash + extname(filename));
}
async function setSrcset(img, src, hash, format, metadataWidth) {
img.setAttribute("srcset", await srcset(src, hash, format, metadataWidth));
}
const processImage = async (el) => {
const filename = el.getAttribute("src");
if (/^(https?:\/\/|\/\/)/i.test(filename)) {
return;
}
if (extname(filename.toLowerCase()) === ".svg") {
return;
}
const file = join(process.cwd(), filename);
// Generate file hash
const hash = MD5(readFileSync(file).toString());
// Get image metadata
const metadata = await sharp(file).metadata();
el.setAttribute("decoding", "async");
el.setAttribute("loading", "lazy");
el.setAttribute("height", metadata.height);
el.setAttribute("width", metadata.width);
const doc = el.ownerDocument;
const picture = doc.createElement("picture");
const webp = doc.createElement("source");
const jpeg = doc.createElement("source");
await setSrcset(webp, filename, hash, "webp", metadata.width);
webp.setAttribute("type", "image/webp");
await setSrcset(jpeg, filename, hash, "jpeg", metadata.width);
jpeg.setAttribute("type", "image/jpeg");
picture.appendChild(webp);
picture.appendChild(jpeg);
el.parentElement.replaceChild(picture, el);
picture.appendChild(el);
copyFileSync(
join(process.cwd(), filename),
join("_site", hashedName(filename, hash))
);
};
const convert = async (rawContent, outputPath) => {
let content = rawContent;
const targetDirectory = "./_site/assets/images";
if (!existsSync(targetDirectory)) {
mkdirSync(targetDirectory, { recursive: true });
}
if (outputPath && outputPath.endsWith(".html")) {
const dom = new JSDOM(content);
const images = [...dom.window.document.querySelectorAll("img")];
if (images.length > 0) {
await Promise.all(images.map((i) => processImage(i, outputPath)));
content = dom.serialize();
}
}
return content;
};
module.exports = {
initArguments: {},
configFunction: async (eleventyConfig = {}) => {
eleventyConfig.addTransform("imageConversion", convert);
}
};