Repository: liamfiddler/eleventy-plugin-lazyimages Branch: master Commit: 8c43de3ea469 Files: 27 Total size: 36.0 KB Directory structure: gitextract_5dca183q/ ├── .editorconfig ├── .eleventy.js ├── .gitignore ├── .npmignore ├── .prettierrc ├── LICENSE ├── cache.js ├── example/ │ ├── basic/ │ │ ├── .eleventy.js │ │ ├── _includes/ │ │ │ └── template.njk │ │ ├── index.md │ │ ├── nested/ │ │ │ └── index.md │ │ └── package.json │ ├── custom-selector/ │ │ ├── .eleventy.js │ │ ├── _includes/ │ │ │ └── template.njk │ │ ├── index.md │ │ └── package.json │ ├── eleventy-plugin-local-images/ │ │ ├── .eleventy.js │ │ ├── _includes/ │ │ │ └── template.njk │ │ ├── index.md │ │ └── package.json │ └── verlok-vanilla-lazyload/ │ ├── .eleventy.js │ ├── _includes/ │ │ └── template.njk │ ├── index.md │ └── package.json ├── helpers.js ├── package.json └── readme.md ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ root = true [*] indent_style = space indent_size = 2 charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true end_of_line = lf max_line_length = null ================================================ FILE: .eleventy.js ================================================ const url = require('url'); const querystring = require('querystring'); const path = require('path'); const { JSDOM } = require('jsdom'); const sharp = require('sharp'); const fetch = require('cross-fetch'); const cache = require('./cache'); const { transformImgPath, logMessage, initScript, checkConfig, } = require('./helpers'); // The default values for the plugin const defaultLazyImagesConfig = { maxPlaceholderWidth: 25, maxPlaceholderHeight: 25, imgSelector: 'img', transformImgPath, className: ['lazyload'], cacheFile: '.lazyimages.json', appendInitScript: true, scriptSrc: 'https://cdn.jsdelivr.net/npm/lazysizes@5/lazysizes.min.js', preferNativeLazyLoad: false, setWidthAndHeightAttrs: true, addNoScript: false, }; // A global to store the current config (saves us passing it around functions) let lazyImagesConfig = defaultLazyImagesConfig; // Reads the image object from the source file const readImage = async (imageSrc) => { let image; if (imageSrc.startsWith('http') || imageSrc.startsWith('//')) { const res = await fetch(imageSrc); const buffer = await res.buffer(); image = await sharp(buffer); return image; } try { image = await sharp(imageSrc); await image.metadata(); // just to confirm it can be read } catch (firstError) { try { // We couldn't read the file at the input path, but maybe it's // in './src', developers love to put things in './src' image = await sharp(`./src/${imageSrc}`); await image.metadata(); } catch (secondError) { throw firstError; } } return image; }; // Gets the image width+height+LQIP from the cache, or generates them if not found const getImageData = async (imageSrc) => { const { maxPlaceholderWidth, maxPlaceholderHeight, cacheFile, } = lazyImagesConfig; let imageData = cache.read(imageSrc); if (imageData) { return imageData; } logMessage(`started processing ${imageSrc}`); const image = await readImage(imageSrc); const metadata = await image.metadata(); const width = metadata.width; const height = metadata.height; const lqip = await image .resize({ width: maxPlaceholderWidth, height: maxPlaceholderHeight, fit: sharp.fit.inside, }) .blur() .toBuffer(); const encodedLqip = lqip.toString('base64'); imageData = { width, height, src: `data:image/png;base64,${encodedLqip}`, }; logMessage(`finished processing ${imageSrc}`); cache.update(cacheFile, imageSrc, imageData); return imageData; }; // Adds the attributes to the image element const processImage = async (imgElem, options) => { const { transformImgPath, className, preferNativeLazyLoad, setWidthAndHeightAttrs, } = lazyImagesConfig; if (preferNativeLazyLoad) { imgElem.setAttribute('loading', 'lazy'); } if (imgElem.src.startsWith('data:')) { logMessage('skipping image with data URI'); return; } const imgPath = transformImgPath(imgElem.src, options); const parsedUrl = url.parse(imgPath); let fileExt = path.extname(parsedUrl.pathname).substr(1); if (!fileExt) { // Twitter and similar pass the file format in the querystring, e.g. "?format=jpg" fileExt = querystring.parse(parsedUrl.query).format || querystring.parse(parsedUrl.query).fm; } imgElem.setAttribute('data-src', imgElem.src); const classNameArr = Array.isArray(className) ? className : [className]; imgElem.classList.add(...classNameArr); if (imgElem.hasAttribute('srcset')) { const srcSet = imgElem.getAttribute('srcset'); imgElem.setAttribute('data-srcset', srcSet); imgElem.removeAttribute('srcset'); } try { const image = await getImageData(imgPath); imgElem.setAttribute('src', image.src); if (!setWidthAndHeightAttrs || fileExt === 'svg') { return; } const widthAttr = imgElem.getAttribute('width'); const heightAttr = imgElem.getAttribute('height'); if (!widthAttr && !heightAttr) { imgElem.setAttribute('width', image.width); imgElem.setAttribute('height', image.height); } else if (widthAttr && !heightAttr) { const ratioHeight = (image.height * widthAttr) / image.width; imgElem.setAttribute('height', Math.round(ratioHeight)); } else if (heightAttr && !widthAttr) { const ratioWidth = (image.width * heightAttr) / image.height; imgElem.setAttribute('width', Math.round(ratioWidth)); } } catch (e) { logMessage(`${e.message}: ${imgPath}`); } }; // Scans the output HTML for images, processes them, & appends the init script async function transformMarkup(rawContent, outputPath) { const { imgSelector, appendInitScript, scriptSrc, preferNativeLazyLoad, addNoScript, } = lazyImagesConfig; let content = rawContent; if (outputPath && outputPath.endsWith('.html')) { const dom = new JSDOM(content); const images = [...dom.window.document.querySelectorAll(imgSelector)]; const params = { outputPath, outputDir: this.outputDir, inputPath: this.inputPath, inputDir: this.inputDir, extraOutputSubdirectory: this.extraOutputSubdirectory, }; if (addNoScript) { Array.from(images).forEach((image) => { const wrapper = dom.window.document.createElement('noscript'); wrapper.classList.add('nojs-image'); wrapper.innerHTML = image.outerHTML; image.parentNode.insertBefore(wrapper, image); wrapper.nextSibling.classList.add('js-image'); }); } if (images.length > 0) { logMessage(`found ${images.length} images in ${outputPath}`); await Promise.all(images.map((image) => processImage(image, params))); logMessage(`processed ${images.length} images in ${outputPath}`); if (appendInitScript) { dom.window.document.body.insertAdjacentHTML( 'beforeend', `` ); } content = dom.serialize(); } } return content; } // Export as 11ty plugin module.exports = { initArguments: {}, configFunction: (eleventyConfig, pluginOptions = {}) => { lazyImagesConfig = { ...defaultLazyImagesConfig, ...pluginOptions, }; checkConfig(lazyImagesConfig, defaultLazyImagesConfig); cache.load(lazyImagesConfig.cacheFile); eleventyConfig.addTransform('lazyimages', transformMarkup); }, }; ================================================ FILE: .gitignore ================================================ # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* # Runtime data pids *.pid *.seed *.pid.lock # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Coverage directory used by tools like istanbul coverage # nyc test coverage .nyc_output # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) .grunt # Bower dependency directory (https://bower.io/) bower_components # node-waf configuration .lock-wscript # Compiled binary addons (https://nodejs.org/api/addons.html) build/Release # Dependency directories node_modules/ jspm_packages/ # TypeScript v1 declaration files typings/ # Optional npm cache directory .npm # Optional eslint cache .eslintcache # Optional REPL history .node_repl_history # Output of 'npm pack' *.tgz # Yarn Integrity file .yarn-integrity # dotenv environment variables file .env # build output .next _site # other junk .DS_Store example/**/package-lock.json example/**/.lazyimages.json /.vscode ================================================ FILE: .npmignore ================================================ example/**/* ================================================ FILE: .prettierrc ================================================ { "singleQuote": true, "trailingComma": "es5" } ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2019 Liam Fiddler 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. ================================================ FILE: cache.js ================================================ const fs = require('fs'); // A global to store the cache data in memory let lazyImagesCache = {}; // Loads the cache data into memory exports.load = (cacheFile) => { if (!cacheFile) { return; } try { if (fs.existsSync(cacheFile)) { const cachedData = fs.readFileSync(cacheFile, 'utf8'); lazyImagesCache = JSON.parse(cachedData); } } catch (e) { console.error('LazyImages - cacheFile', e); } }; // Reads the cached data for an image exports.read = (imageSrc) => { if (imageSrc in lazyImagesCache) { return lazyImagesCache[imageSrc]; } return undefined; }; // Updates image data in the cache exports.update = (cacheFile, imageSrc, imageData) => { lazyImagesCache[imageSrc] = imageData; if (cacheFile) { const cacheData = JSON.stringify(lazyImagesCache); fs.writeFile(cacheFile, cacheData, (err) => { if (err) { console.error('LazyImages - cacheFile', err); } }); } }; ================================================ FILE: example/basic/.eleventy.js ================================================ const lazyImagesPlugin = require('eleventy-plugin-lazyimages'); module.exports = function(eleventyConfig) { eleventyConfig.addPassthroughCopy('img'); eleventyConfig.addPassthroughCopy('nested'); eleventyConfig.addPlugin(lazyImagesPlugin); }; ================================================ FILE: example/basic/_includes/template.njk ================================================


## These images are from a third-party website:













CC0 images from [Flickr](https://www.flickr.com/search/?license=9)
================================================
FILE: example/basic/nested/index.md
================================================
---
layout: template.njk
title: Basic demo - eleventy-plugin-lazyimages
---
# Nested relative path demo


================================================
FILE: example/basic/package.json
================================================
{
"name": "eleventy-plugin-lazyimages-example-basic",
"version": "1.0.0",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "eleventy --serve",
"build": "eleventy"
},
"author": "@liamfiddler",
"license": "MIT",
"devDependencies": {
"@11ty/eleventy": "^0.11.0",
"eleventy-plugin-lazyimages": "../../"
}
}
================================================
FILE: example/custom-selector/.eleventy.js
================================================
const lazyImagesPlugin = require('eleventy-plugin-lazyimages');
module.exports = function(eleventyConfig) {
eleventyConfig.addPlugin(lazyImagesPlugin, {
imgSelector: '.lazyimages img',
});
};
================================================
FILE: example/custom-selector/_includes/template.njk
================================================












CC0 images from [Flickr](https://www.flickr.com/search/?license=9)
================================================
FILE: example/verlok-vanilla-lazyload/package.json
================================================
{
"name": "eleventy-plugin-lazyimages-example-verlok",
"version": "1.0.0",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "eleventy --serve",
"build": "eleventy"
},
"author": "@liamfiddler",
"license": "MIT",
"devDependencies": {
"@11ty/eleventy": "^0.11.0",
"eleventy-plugin-lazyimages": "../../"
}
}
================================================
FILE: helpers.js
================================================
const path = require('path');
// Ensures relative paths start in the project root
exports.transformImgPath = (src, options = {}) => {
if (
src.startsWith('./') ||
src.startsWith('../') ||
(!src.startsWith('/') &&
!src.startsWith('http://') &&
!src.startsWith('https://') &&
!src.startsWith('data:'))
) {
// The file path is relative to the output document
const outputDir = path.posix.parse(options.inputPath).dir;
return path.posix.normalize(outputDir + '/' + src);
}
// Reference files from the root project directory
if (src.startsWith('/') && !src.startsWith('//')) {
return `.${src}`;
}
return src;
};
// Logs a message prepended with "LazyImages - "
exports.logMessage = (message) => {
console.log(`LazyImages - ${message}`);
};
// Init script for the plugin that gets injected into the final markup
// (we have to use lowest common denominator JS language features
// because we don't know what the target browser support is)
exports.initScript = function (selector, src, preferNativeLazyLoad) {
var images = document.querySelectorAll(selector);
var numImages = images.length;
if (numImages > 0) {
if (preferNativeLazyLoad && 'loading' in HTMLImageElement.prototype) {
for (var i = 0; i < numImages; i++) {
var keys = ['src', 'srcset'];
for (var j = 0; j < keys.length; j++) {
if (images[i].hasAttribute('data-' + keys[j])) {
var value = images[i].getAttribute('data-' + keys[j]);
images[i].setAttribute(keys[j], value);
}
}
}
return;
}
var script = document.createElement('script');
script.async = true;
script.src = src;
document.body.appendChild(script);
}
};
// Warns about common issues with custom configs
exports.checkConfig = (config, defaultConfig) => {
const { appendInitScript, className } = config;
const isDefaultScriptSrc = config.scriptSrc === defaultConfig.scriptSrc;
if (!isDefaultScriptSrc && !appendInitScript) {
console.warn(
'LazyImages - scriptSrc will be ignored because appendInitScript=false'
);
}
if (
isDefaultScriptSrc &&
appendInitScript &&
!className.includes('lazyload')
) {
console.warn(
'LazyImages - LazySizes with the default config requires "lazyload" be included in className'
);
}
};
================================================
FILE: package.json
================================================
{
"name": "eleventy-plugin-lazyimages",
"version": "2.1.2",
"description": "An 11ty plugin that adds lazy loading to your images",
"main": ".eleventy.js",
"scripts": {
"test": "echo \"Error: no test specified\""
},
"repository": {
"type": "git",
"url": "git+https://github.com/liamfiddler/eleventy-plugin-lazyimages.git"
},
"keywords": [
"11ty",
"eleventy",
"eleventy-plugin",
"plugin",
"lazy",
"lazyload",
"image"
],
"author": "@liamfiddler",
"license": "MIT",
"bugs": {
"url": "https://github.com/liamfiddler/eleventy-plugin-lazyimages/issues"
},
"homepage": "https://github.com/liamfiddler/eleventy-plugin-lazyimages#readme",
"dependencies": {
"cross-fetch": "^3.1.4",
"jsdom": ">=18.1.1",
"sharp": ">=0.29.3"
}
}
================================================
FILE: readme.md
================================================
# LazyImages plugin for [11ty](https://www.11ty.io/)

What this plugin does:
- 🔍 Finds IMG elements in your markup
- ➕ Adds width and height attributes to the element
- ✋ Defers loading the image until it is in/near the viewport
(lazy loading)
- 🖼️ Displays a blurry low-res placeholder until the image has loaded
(LQIP)
This plugin supports:
- Any 11ty template format that outputs to a .html file
- Absolute image paths
- Relative image paths (improved in v2.1!)
- Custom image selectors; target all images or only images in a certain part
of the page
- Placeholder generation for all image formats supported by
[Sharp](https://sharp.pixelplumbing.com/); including JPEG, PNG, WebP, TIFF, GIF, & SVG
- Responsive images using `srcset`; the image in the `src` attribute will be
used for determining the placeholder image and width/height attributes
---
**v2.1 just released! [View the release/upgrade notes](#upgrade-notes)**
---
**Like this project? Buy me a coffee via [PayPal](https://paypal.me/liamfiddler) or [ko-fi](https://ko-fi.com/liamfiddler)**
---
## Getting started
### Install the plugin
In your project directory run:
```sh
# Using npm
npm install eleventy-plugin-lazyimages --save-dev
# Or using yarn
yarn add eleventy-plugin-lazyimages --dev
```
Then update your project's `.eleventy.js` to include the plugin:
```js
const lazyImagesPlugin = require('eleventy-plugin-lazyimages');
module.exports = function (eleventyConfig) {
eleventyConfig.addPlugin(lazyImagesPlugin);
};
```
### Tweak your CSS (optional)
This plugin will automatically set the width and height attributes
for each image based on the source image dimensions. You might want
to overwrite this with the following CSS:
```css
img {
display: block;
width: 100%;
max-width: 100%;
height: auto;
}
```
The above CSS will ensure the image is never wider than its
container and the aspect ratio is maintained.
### Configure the plugin (optional)
You can pass an object with configuration options as the second
parameter:
```js
eleventyConfig.addPlugin(lazyImagesPlugin, {
imgSelector: '.post-content img', // custom image selector
cacheFile: '', // don't cache results to a file
});
```
A full list of available configuration options are listed below,
and some common questions are covered at the end of this file.
## Configuration options
| Key | Type | Description |
| ------------------------ | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `maxPlaceholderWidth` | Integer | The maximum width in pixels of the generated placeholder image. Recommended values are between 15 and 40.
` at `
` or `
` is expected to
be found at `/images/dog.jpg`.
If you prefer to store your images elsewhere the `transformImgPath` config
option allows you to specify a function that points the plugin to your
internal image path.
For example, if your file structure stores `
`
at `