metalsmith-optimize-images 0.1.1 → 0.9.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,25 +1,31 @@
1
1
  # metalsmith-optimize-images
2
2
 
3
- > **⚠️: This plugin is a fully functional proof-of-concept. However, it is not yet fully tested and may contain bugs. Use with caution.**
3
+ > **🚀 Release Candidate**: This plugin has achieved comprehensive test coverage (95.27%) and is ready for production testing. Recent critical bug fixes have resolved recursive processing and AVIF format issues. Feedback welcome before 1.0.0 release!
4
4
 
5
5
  Metalsmith plugin for generating responsive images with optimal formats
6
6
 
7
7
  [![metalsmith:plugin][metalsmith-badge]][metalsmith-url]
8
8
  [![npm: version][npm-badge]][npm-url]
9
9
  [![license: MIT][license-badge]][license-url]
10
- [![coverage][coverage-badge]][coverage-url]
10
+ [![test coverage][coverage-badge]][coverage-url]
11
11
  [![ESM/CommonJS][modules-badge]][npm-url]
12
+ [![Known Vulnerabilities](https://snyk.io/test/npm/metalsmith-optimize-images/badge.svg)](https://snyk.io/test/npm/metalsmith-optimize-images)
12
13
 
13
14
  ## Features
14
15
 
15
16
  - **Multiple image formats**: Generates AVIF and WebP variants with JPEG/PNG fallbacks
16
17
  - **Responsive sizes**: Creates different image sizes for various device widths
18
+ - **Background image support**: Automatically processes unused images for CSS `image-set()` backgrounds
19
+ - **Progressive loading**: Optional progressive image loading with low-quality placeholders
17
20
  - **Lazy loading**: Uses native browser lazy loading for better performance
18
21
  - **Content-based hashing**: Adds hash to filenames for optimal caching
19
22
  - **Layout shift prevention**: Adds width/height attributes
20
23
  - **Parallel processing**: Processes images in parallel for faster builds
21
- - **Metadata generation**: Creates a JSON file with image information
24
+ - **Metadata generation**: Creates a JSON manifest with image information and variants
22
25
  - **Configurable compression**: Customize compression settings per format
26
+ - **ESM and CommonJS support**:
27
+ - ESM: `import optimizeImages from 'metalsmith-optimize-images'`
28
+ - CommonJS: `const optimizeImages = require('metalsmith-optimize-images')`
23
29
 
24
30
  ## Installation
25
31
 
@@ -27,60 +33,61 @@ Metalsmith plugin for generating responsive images with optimal formats
27
33
  npm install metalsmith-optimize-images
28
34
  ```
29
35
 
30
- ### Requirements
36
+ ## Requirements
31
37
 
32
- - Node.js 18.0.0 or newer
33
- - Metalsmith 2.5.0 or newer
38
+ - Node.js >=18.0.0
39
+ - Metalsmith >=2.5.0
34
40
 
35
- ## Usage
41
+ ## Platform Testing Status
36
42
 
37
- ### ESM
43
+ - ✅ **macOS**: Fully tested and working
44
+ - 🔄 **Windows**: Seeking community feedback on Sharp.js compatibility
45
+ - 🔄 **Linux**: Seeking validation across different distributions
46
+ - 🔄 **CI/CD**: Testing in various containerized environments
38
47
 
39
- ```javascript
40
- import metalsmith from 'metalsmith';
41
- import optimizeImages from 'metalsmith-optimize-images';
48
+ **Help us reach 1.0.0**: If you test this plugin on Windows, Linux, or in production environments, please [share your experience](https://github.com/wernerglinka/metalsmith-optimize-images/issues)!
42
49
 
43
- metalsmith.use(
44
- optimizeImages({
45
- // configuration options
46
- widths: [320, 640, 960, 1280, 1920],
47
- formats: ['avif', 'webp', 'original']
48
- })
49
- );
50
- ```
50
+ ## Usage
51
51
 
52
- ### CommonJS
52
+ > This plugin **must** be run after assets are copied but before any final HTML processing.
53
53
 
54
54
  ```javascript
55
- const metalsmith = require('metalsmith');
56
- const optimizeImages = require('metalsmith-optimize-images');
57
-
58
- metalsmith.use(
59
- optimizeImages({
60
- // configuration options
61
- widths: [320, 640, 960, 1280, 1920],
62
- formats: ['avif', 'webp', 'original']
63
- })
64
- );
55
+ metalsmith
56
+ .use(
57
+ assets({
58
+ source: 'lib/assets/', // Where to find assets
59
+ destination: 'assets/' // Where to copy assets
60
+ })
61
+ )
62
+ .use(
63
+ optimizeImages({
64
+ // configuration options
65
+ widths: [320, 640, 960, 1280, 1920],
66
+ formats: ['avif', 'webp', 'original']
67
+ })
68
+ );
65
69
  ```
66
70
 
67
71
  ## Options
68
72
 
69
- | Option | Type | Default | Description |
70
- | --------------------- | ---------- | ------------------------------------- | ---------------------------------------- |
71
- | `widths` | `number[]` | `[320, 640, 960, 1280, 1920]` | Image sizes to generate |
72
- | `formats` | `string[]` | `['avif', 'webp', 'original']` | Image formats in order of preference |
73
- | `formatOptions` | `object` | See below | Format-specific compression settings |
74
- | `htmlPattern` | `string` | `**/*.html` | Glob pattern to match HTML files |
75
- | `imgSelector` | `string` | `img:not([data-no-responsive])` | CSS selector for images to process |
76
- | `outputDir` | `string` | `assets/images/responsive` | Where to store the responsive images |
77
- | `outputPattern` | `string` | `[filename]-[width]w-[hash].[format]` | Filename pattern with tokens |
78
- | `skipLarger` | `boolean` | `true` | Don't upscale images |
79
- | `lazy` | `boolean` | `true` | Use native lazy loading |
80
- | `dimensionAttributes` | `boolean` | `true` | Add width/height to prevent layout shift |
81
- | `sizes` | `string` | `(max-width: 768px) 100vw, 75vw` | Default sizes attribute |
82
- | `concurrency` | `number` | `5` | Process N images at a time |
83
- | `generateMetadata` | `boolean` | `false` | Generate a metadata JSON file |
73
+ | Option | Type | Default | Description |
74
+ | --------------------- | ---------- | ------------------------------------- | ------------------------------------------------------------------------------ |
75
+ | `widths` | `number[]` | `[320, 640, 960, 1280, 1920]` | Image sizes to generate |
76
+ | `formats` | `string[]` | `['avif', 'webp', 'original']` | Image formats in order of preference |
77
+ | `formatOptions` | `object` | See below | Format-specific compression settings |
78
+ | `htmlPattern` | `string` | `**/*.html` | Glob pattern to match HTML files |
79
+ | `imgSelector` | `string` | `img:not([data-no-responsive])` | CSS selector for images to process |
80
+ | `outputDir` | `string` | `assets/images/responsive` | Where to store the responsive images |
81
+ | `outputPattern` | `string` | `[filename]-[width]w-[hash].[format]` | Filename pattern with tokens |
82
+ | `skipLarger` | `boolean` | `true` | Don't upscale images |
83
+ | `lazy` | `boolean` | `true` | Use native lazy loading |
84
+ | `dimensionAttributes` | `boolean` | `true` | Add width/height to prevent layout shift |
85
+ | `sizes` | `string` | `(max-width: 768px) 100vw, 75vw` | Default sizes attribute |
86
+ | `concurrency` | `number` | `5` | Process N images at a time |
87
+ | `generateMetadata` | `boolean` | `false` | Generate a metadata JSON file at `{outputDir}/responsive-images-manifest.json` |
88
+ | `isProgressive` | `boolean` | `false` | Enable progressive image loading |
89
+ | `placeholder` | `object` | See below | Placeholder image settings |
90
+ | `processUnusedImages` | `boolean` | `true` | Process unused images for background use |
84
91
 
85
92
  ### Default Format Options
86
93
 
@@ -93,9 +100,23 @@ metalsmith.use(
93
100
  }
94
101
  ```
95
102
 
103
+ ### Default Placeholder Options
104
+
105
+ ```javascript
106
+ {
107
+ width: 50, // Width of placeholder image
108
+ quality: 30, // Quality of placeholder image
109
+ blur: 10 // Blur amount for placeholder
110
+ }
111
+ ```
112
+
96
113
  ## How It Works
97
114
 
98
- The plugin:
115
+ ### Standard Mode (default)
116
+
117
+ The plugin operates in two phases:
118
+
119
+ **Phase 1: HTML-Referenced Images**
99
120
 
100
121
  1. Scans HTML files for image tags
101
122
  2. Processes each image to create multiple sizes and formats
@@ -104,6 +125,25 @@ The plugin:
104
125
  5. Adds width/height attributes to prevent layout shifts
105
126
  6. Implements native lazy loading for better performance
106
127
 
128
+ **Phase 2: Background Images (when `processUnusedImages: true`)**
129
+
130
+ 1. Finds images that weren't processed in Phase 1
131
+ 2. Generates 1x/2x variants (half size and original size) for retina displays
132
+ 3. Creates all configured formats (AVIF, WebP, original)
133
+ 4. Suitable for use with CSS `image-set()` for background images
134
+
135
+ ### Progressive Mode (experimental)
136
+
137
+ When `isProgressive: true` is enabled:
138
+
139
+ 1. Generates low-quality placeholder images (small, blurred)
140
+ 2. Creates wrapper elements with both placeholder and high-resolution images
141
+ 3. Uses Intersection Observer to load high-resolution images on demand
142
+ 4. Implements smooth transitions between placeholder and final image
143
+ 5. Uses modern `createImageBitmap()` for reliable format detection (AVIF/WebP support)
144
+ 6. Maintains proper aspect ratios using original image dimensions
145
+ 7. Provides CSS and JavaScript for progressive loading behavior
146
+
107
147
  ## Examples
108
148
 
109
149
  ### Basic usage with defaults
@@ -135,12 +175,36 @@ metalsmith.use(
135
175
  // Custom output directory
136
176
  outputDir: 'images/processed',
137
177
 
178
+ // Generate metadata manifest
179
+ generateMetadata: true, // Creates images/processed/responsive-images-manifest.json
180
+
138
181
  // Don't add lazy loading
139
182
  lazy: false
140
183
  })
141
184
  );
142
185
  ```
143
186
 
187
+ ### Progressive loading configuration
188
+
189
+ ```javascript
190
+ metalsmith.use(
191
+ optimizeImages({
192
+ // Enable progressive loading
193
+ isProgressive: true,
194
+
195
+ // Customize placeholder settings
196
+ placeholder: {
197
+ width: 40, // Smaller placeholder
198
+ quality: 20, // Lower quality for faster loading
199
+ blur: 15 // More blur for artistic effect
200
+ },
201
+
202
+ // Progressive mode works best with original format only
203
+ formats: ['original']
204
+ })
205
+ );
206
+ ```
207
+
144
208
  ### Excluding specific images
145
209
 
146
210
  Add the `data-no-responsive` attribute to any image you don't want processed:
@@ -149,46 +213,217 @@ Add the `data-no-responsive` attribute to any image you don't want processed:
149
213
  <img src="image.jpg" data-no-responsive alt="This image won't be processed" />
150
214
  ```
151
215
 
152
- ## Test Coverage
216
+ ## Background Images
153
217
 
154
- This project maintains a high level of test coverage to ensure reliability.
218
+ The plugin automatically processes images that aren't referenced in HTML for use as CSS background images. This feature is enabled by default (`processUnusedImages: true`).
155
219
 
156
- ## Debug
220
+ ### How Background Processing Works
157
221
 
158
- For debugging, use Metalsmith's debug mode:
222
+ After processing HTML-referenced images, the plugin:
159
223
 
160
- ```javascript
161
- // Enable debug output by setting the DEBUG environment variable
162
- // DEBUG=metalsmith-responsive-images node build.js
224
+ 1. **Scans the Metalsmith files object** for all images
225
+ 2. **Excludes already-processed images** (those found during HTML scanning)
226
+ 3. **Excludes responsive variants** (generated images in the outputDir)
227
+ 4. **Generates 1x/2x variants** using actual image dimensions:
228
+ - **1x variant**: Half the original size for regular displays
229
+ - **2x variant**: Original image size for retina displays (sharper on high-DPI screens)
230
+ 5. **Creates all formats** (AVIF, WebP, original) for optimal browser support
231
+ 6. **No HTML replacement** - you manually write CSS with `image-set()`
232
+
233
+ ### Using Background Images with CSS
234
+
235
+ For an image like `images/hero.jpg` (1920x1080 pixels), the plugin generates variants like:
163
236
 
164
- const metalsmith = Metalsmith(__dirname).use(optimizeImages());
165
- // other plugins...
237
+ ```
238
+ assets/images/responsive/hero-960w.avif (1x - half 960px width for regular displays)
239
+ assets/images/responsive/hero-1920w.avif (2x - original 1920px width, sharper on retina)
240
+ assets/images/responsive/hero-960w.webp (1x - half 960px width for regular displays)
241
+ assets/images/responsive/hero-1920w.webp (2x - original 1920px width, sharper on retina)
242
+ assets/images/responsive/hero-960w.jpg (1x - half 960px width for regular displays)
243
+ assets/images/responsive/hero-1920w.jpg (2x - original 1920px width, sharper on retina)
166
244
  ```
167
245
 
168
- You can also use the [metalsmith-debug](https://github.com/metalsmith/metalsmith-debug) plugin:
246
+ **Note**: Background images are generated **without hashes** for easier CSS authoring. HTML images still include hashes for cache-busting.
247
+
248
+ Use them in CSS with `image-set()`:
249
+
250
+ ```css
251
+ .hero {
252
+ background-image: image-set(
253
+ url('/assets/images/responsive/hero-960w.avif') 1x,
254
+ url('/assets/images/responsive/hero-1920w.avif') 2x,
255
+ url('/assets/images/responsive/hero-960w.webp') 1x,
256
+ url('/assets/images/responsive/hero-1920w.webp') 2x,
257
+ url('/assets/images/responsive/hero-960w.jpg') 1x,
258
+ url('/assets/images/responsive/hero-1920w.jpg') 2x
259
+ );
260
+ background-size: cover;
261
+ background-position: center;
262
+ }
263
+ ```
264
+
265
+ ### Background Image Configuration
169
266
 
170
267
  ```javascript
171
- const debug = require('metalsmith-debug');
268
+ metalsmith.use(
269
+ optimizeImages({
270
+ // Standard HTML image processing
271
+ widths: [320, 640, 960, 1280, 1920],
272
+ formats: ['avif', 'webp', 'original'],
273
+
274
+ // Background image processing
275
+ processUnusedImages: true, // Enable background processing
172
276
 
173
- const metalsmith = Metalsmith(__dirname)
174
- .use(debug()) // Add the metalsmith-debug plugin
175
- .use(optimizeImages());
176
- // other plugins...
277
+ // Generate metadata to see all variants
278
+ generateMetadata: true
279
+ })
280
+ );
177
281
  ```
178
282
 
179
- Or with the CLI:
283
+ ### Benefits of Background Image Processing
284
+
285
+ - **Automatic format optimization** - Browser selects best supported format
286
+ - **Retina display support** - 2x variants provide crisp images on high-DPI screens
287
+ - **Smart sizing** - Uses actual image dimensions instead of arbitrary widths
288
+ - **No manual work** - Plugin automatically finds and processes unused images in Metalsmith files object
289
+ - **Consistent workflow** - Same formats and quality settings as HTML images
290
+ - **Efficient processing** - Parallel processing of sizes and formats
291
+
292
+ ## Progressive Loading
293
+
294
+ ### Overview
295
+
296
+ Progressive loading provides a smooth user experience by:
297
+
298
+ 1. **Immediate display**: Shows a low-quality placeholder instantly
299
+ 2. **Smooth transitions**: Fades from placeholder to high-quality image
300
+ 3. **Lazy loading**: Only loads high-resolution images when they enter the viewport
301
+ 4. **Format optimization**: Automatically serves the best supported format
302
+
303
+ ### Implementation
304
+
305
+ When progressive mode is enabled, the plugin:
306
+
307
+ - Generates small, blurred placeholder images
308
+ - Creates wrapper elements with proper aspect ratios
309
+ - Includes JavaScript for intersection observer-based loading
310
+ - Provides CSS for smooth transitions
311
+
312
+ ### HTML Output
313
+
314
+ **Standard mode:**
315
+
316
+ ```html
317
+ <picture>
318
+ <source
319
+ type="image/avif"
320
+ srcset="image-320w.avif 320w, image-640w.avif 640w"
321
+ sizes="(max-width: 768px) 100vw, 75vw"
322
+ />
323
+ <source
324
+ type="image/webp"
325
+ srcset="image-320w.webp 320w, image-640w.webp 640w"
326
+ sizes="(max-width: 768px) 100vw, 75vw"
327
+ />
328
+ <img
329
+ src="image-640w.jpg"
330
+ srcset="image-320w.jpg 320w, image-640w.jpg 640w"
331
+ sizes="(max-width: 768px) 100vw, 75vw"
332
+ alt="Description"
333
+ loading="lazy"
334
+ />
335
+ </picture>
336
+ ```
337
+
338
+ **Progressive mode:**
339
+
340
+ ```html
341
+ <div class="responsive-wrapper js-progressive-image-wrapper" style="aspect-ratio: 1280/720">
342
+ <img class="low-res" src="/assets/images/responsive/image-placeholder.jpg" alt="Description" />
343
+ <img class="high-res" src="" alt="Description" data-source="/assets/images/responsive/image-960w.jpg" />
344
+ </div>
345
+ ```
346
+
347
+ ### CSS Requirements
348
+
349
+ The plugin provides CSS for progressive loading, but you can customize it:
350
+
351
+ ```css
352
+ .responsive-wrapper {
353
+ position: relative;
354
+ overflow: hidden;
355
+ background-color: #f0f0f0;
356
+ }
357
+
358
+ .responsive-wrapper .low-res {
359
+ position: absolute;
360
+ top: 0;
361
+ left: 0;
362
+ width: 100%;
363
+ height: 100%;
364
+ object-fit: cover;
365
+ transition: opacity 0.4s ease;
366
+ }
367
+
368
+ .responsive-wrapper .high-res {
369
+ position: absolute;
370
+ top: 0;
371
+ left: 0;
372
+ width: 100%;
373
+ height: 100%;
374
+ object-fit: cover;
375
+ opacity: 0;
376
+ transition: opacity 0.4s ease;
377
+ }
378
+
379
+ .responsive-wrapper.done .high-res {
380
+ opacity: 1;
381
+ }
382
+
383
+ .responsive-wrapper.done .low-res {
384
+ opacity: 0;
385
+ }
386
+ ```
387
+
388
+ ## Metadata Manifest
389
+
390
+ When `generateMetadata: true` is enabled, the plugin creates a JSON file at `{outputDir}/responsive-images-manifest.json` containing detailed information about all processed images:
180
391
 
181
392
  ```json
182
393
  {
183
- "plugins": {
184
- "metalsmith-debug": true,
185
- "metalsmith-optimize-images": {
186
- "widths": [320, 640, 960, 1280, 1920]
394
+ "images/hero.jpg": [
395
+ {
396
+ "path": "assets/images/responsive/hero-320w-a1b2c3d4.avif",
397
+ "width": 320,
398
+ "height": 180,
399
+ "format": "avif",
400
+ "size": 8432
401
+ },
402
+ {
403
+ "path": "assets/images/responsive/hero-320w-a1b2c3d4.webp",
404
+ "width": 320,
405
+ "height": 180,
406
+ "format": "webp",
407
+ "size": 12658
187
408
  }
188
- }
409
+ ]
189
410
  }
190
411
  ```
191
412
 
413
+ This manifest is useful for:
414
+
415
+ - **Debugging**: Verify which variants were generated
416
+ - **Integration**: Use variant information in other tools
417
+ - **Performance analysis**: Compare file sizes across formats
418
+
419
+ ## Debug
420
+
421
+ To enable debug logs, set the DEBUG environment variable to metalsmith-optimize-images\*:
422
+
423
+ ```javascript
424
+ metalsmith.env('DEBUG', 'metalsmith-optimize-images*');
425
+ ```
426
+
192
427
  ## CLI Usage
193
428
 
194
429
  ### Metalsmith CLI
@@ -204,6 +439,36 @@ Or with the CLI:
204
439
  }
205
440
  ```
206
441
 
442
+ ## Recent Updates
443
+
444
+ ### Bug Fixes (January 2025)
445
+
446
+ - **🚫 Fixed Recursive Processing**: Resolved critical issue where the background image processor was finding already-generated responsive images and reprocessing them recursively, creating malformed filenames like `image-320w-640w-960w.jpg`
447
+ - **🚫 Fixed HEIF Extension Issue**: Fixed Sharp.js AVIF processing that was sometimes generating `.heif` extensions instead of `.avif`
448
+ - **✅ Enhanced Background Image Filtering**: Added comprehensive filtering to prevent responsive variants from being treated as source images
449
+
450
+ ## Feedback & Testing
451
+
452
+ This plugin is approaching 1.0.0 and we'd love your feedback! Please test and report:
453
+
454
+ ### Especially Needed
455
+
456
+ - **Windows compatibility**: Sharp.js native compilation and image processing
457
+ - **Large image batches**: Performance with 50+ images
458
+ - **Memory usage**: Resource consumption in your environment
459
+ - **Cross-platform consistency**: Image quality and file sizes across platforms
460
+ - **Progressive loading**: Behavior across different browsers
461
+ - **Background image processing**: Testing the new `image-set()` feature with CSS backgrounds
462
+
463
+ ### Current Status
464
+
465
+ - ✅ 95.27% test coverage with comprehensive edge case handling
466
+ - ✅ Real Metalsmith integration tests (no mocks)
467
+ - ✅ Tested on macOS with Node.js 18+
468
+ - 🔄 Seeking broader platform validation
469
+
470
+ **Report issues or success stories**: [GitHub Issues](https://github.com/wernerglinka/metalsmith-optimize-images/issues)
471
+
207
472
  ## License
208
473
 
209
474
  MIT
@@ -215,5 +480,5 @@ MIT
215
480
  [license-badge]: https://img.shields.io/github/license/wernerglinka/metalsmith-optimize-images
216
481
  [license-url]: LICENSE
217
482
  [coverage-badge]: https://img.shields.io/badge/test%20coverage-95%25-brightgreen
218
- [coverage-url]: #test-coverage
483
+ [coverage-url]: https://github.com/wernerglinka/metalsmith-optimize-images/actions/workflows/test.yml
219
484
  [modules-badge]: https://img.shields.io/badge/modules-ESM%2FCJS-blue