diff --git a/package-lock.json b/package-lock.json index 6f161eced..5990c6785 100644 --- a/package-lock.json +++ b/package-lock.json @@ -42,6 +42,7 @@ "remark-lint-strong-marker": "^3.1.1", "remark-lint-unordered-list-marker-style": "^3.1.1", "rollup-stream": "^1.24.1", + "sharp": "^0.32.6", "stylelint": "^15.6.0", "stylelint-order": "^6.0.1", "typograf": "^7.0.0", diff --git a/package.json b/package.json index 7023bd7dd..d7ea47f28 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "remark-lint-strong-marker": "^3.1.1", "remark-lint-unordered-list-marker-style": "^3.1.1", "rollup-stream": "^1.24.1", + "sharp": "^0.32.6", "stylelint": "^15.6.0", "stylelint-order": "^6.0.1", "typograf": "^7.0.0", diff --git a/src/eleventy-config/transforms.js b/src/eleventy-config/transforms.js index 32a097432..01af79f64 100644 --- a/src/eleventy-config/transforms.js +++ b/src/eleventy-config/transforms.js @@ -3,11 +3,44 @@ const htmlmin = require('html-minifier'); const prettydata = require('pretty-data'); const { parseHTML } = require('linkedom'); const Image = require('@11ty/eleventy-img'); +const sharp = require('sharp'); Image.concurrency = require('os').cpus().length; const isProdMode = process.env.NODE_ENV === 'production'; +async function processImage({ imageElement, inputPath, options, attributes }) { + const imageStats = await Image(inputPath, { + filenameFormat: (hash, src, width, format) => { + const extension = path.extname(src); + const name = path.basename(src, extension); + return `${hash}-${name}-${width}.${format}`; + }, + ...options, + }); + + const imageAttributes = Object.assign( + {}, + attributes ?? {}, + Object.fromEntries( + [...imageElement.attributes].map((attr) => [attr.name, attr.value]) + ) + ); + + const tempElement = imageElement.ownerDocument.createElement('div'); + tempElement.innerHTML = Image.generateHTML(imageStats, imageAttributes); + + // Задаём размеры сами, так как для Gif они вычисляются некорректно + // https://github.com/11ty/eleventy-img/pull/182 + const sharpImageMetaData = await sharp(inputPath).metadata(); + const width = imageElement.getAttribute('width') ?? sharpImageMetaData.width; + const height = imageElement.getAttribute('height') ?? sharpImageMetaData.pageHeight ?? sharpImageMetaData.height; + const newImage = tempElement.querySelector('img'); + Object.assign(newImage, { width, height }); + + imageElement.replaceWith(tempElement.firstElementChild); +} + module.exports = function(eleventyConfig) { // преобразование контентных изображений eleventyConfig.addTransform('optimizeContentImages', async function(content) { @@ -32,23 +65,26 @@ module.exports = function(eleventyConfig) { await Promise.all(images.map(async(image) => { const fullImagePath = path.join(articleSourceFolder, image.src); - const imageStats = await Image(fullImagePath, { - widths: ['auto', 600, 1200, 2400], - formats: isProdMode - ? ['svg', 'avif', 'webp', 'auto'] - : ['svg', 'webp', 'auto'], - outputDir: outputArticleImagesFolder, - urlPath: 'images/', - svgShortCircuit: true, - filenameFormat: (hash, src, width, format) => { - const extension = path.extname(src); - const name = path.basename(src, extension); - return `${hash}-${name}-${width}.${format}`; + const isGif = path.extname(fullImagePath) === '.gif'; + + await processImage({ + document, + imageElement: image, + inputPath: fullImagePath, + options: { + widths: ['auto', 600, 1200, 2400], + // `sharp`, на данный момент, не поддерживает анимированный avif + formats: isProdMode && !isGif + ? ['svg', 'avif', 'webp', 'auto'] + : ['svg', 'webp', 'auto'], + outputDir: outputArticleImagesFolder, + urlPath: 'images/', + svgShortCircuit: true, + sharpOptions: { + animated: true, + }, }, - }); - - const imageAttributes = Object.assign( - { + attributes: { loading: 'lazy', decoding: 'async', sizes: [ @@ -58,44 +94,34 @@ module.exports = function(eleventyConfig) { 'calc(100vw - 2 * 16px)', ].join(','), }, - Object.fromEntries( - [...image.attributes].map((attr) => [attr.name, attr.value]) - ) - ); - - const newImageHTML = Image.generateHTML(imageStats, imageAttributes); - image.outerHTML = newImageHTML; + }); })); return document.toString(); }); // преобразование аватаров - { - const avatarImageFormats = isProdMode - ? ['avif', 'webp', 'jpeg'] - : ['webp', 'jpeg']; - - const formatsOrder = ['avif', 'webp', 'jpeg']; - - eleventyConfig.addTransform('optimizeAvatarImages', async function(content) { - if (!this.page.outputPath.endsWith?.('.html')) { - return content; - } + eleventyConfig.addTransform('optimizeAvatarImages', async function(content) { + if (!this.page.outputPath.endsWith?.('.html')) { + return content; + } - const { document } = parseHTML(content); - const images = Array.from(document.querySelectorAll('.blob__photo')) - .filter((image) => !image.src.match(/^https?:/)); + const { document } = parseHTML(content); + const images = Array.from(document.querySelectorAll('.blob__photo')) + .filter((image) => !image.src.match(/^https?:/)); - if (images.length === 0) { - return content; - } + if (images.length === 0) { + return content; + } - await Promise.all(images.map(async(image) => { - const fullImagePath = path.join(eleventyConfig.dir.input, image.src); - const avatarsOutputFolder = path.dirname(path.join(eleventyConfig.dir.output, image.src)); + await Promise.all(images.map(async(image) => { + const fullImagePath = path.join(eleventyConfig.dir.input, image.src); + const avatarsOutputFolder = path.dirname(path.join(eleventyConfig.dir.output, image.src)); - const imageStats = await Image(fullImagePath, { + await processImage({ + imageElement: image, + inputPath: fullImagePath, + options: { widths: image.sizes .split(',') .flatMap((entry) => { @@ -103,38 +129,18 @@ module.exports = function(eleventyConfig) { entry = parseFloat(entry); return [entry, entry * 2]; }), - formats: avatarImageFormats, + formats: isProdMode + ? ['svg', 'avif', 'webp', 'auto'] + : ['svg', 'webp', 'auto'], outputDir: avatarsOutputFolder, urlPath: image.src.split('/').slice(0, -1).join('/'), svgShortCircuit: true, - filenameFormat: (hash, src, width, format) => { - const extension = path.extname(src); - const name = path.basename(src, extension); - return `${hash}-${name}-${width}.${format}`; - }, - }); - - image.outerHTML = ` - - ${ - formatsOrder - .map(((format) => imageStats[format])) - .filter(Boolean) - .map((stats) => { - const type = stats[0].sourceType; - const srcset = stats.map((statsItem) => statsItem.srcset).join(','); - return ``; - }) - .join('') - } - ${image.outerHTML} - - `; - })); - - return document.toString(); - }); - } + }, + }); + })); + + return document.toString(); + }); if (isProdMode) { eleventyConfig.addTransform('htmlmin', (content, outputPath) => {