metalsmith-optimize-images 0.10.2 → 0.12.0

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
@@ -11,10 +11,13 @@ Metalsmith plugin for generating responsive images with optimal formats
11
11
 
12
12
  > This Metalsmith plugin is under active development. The API is stable, but breaking changes may occur before reaching 1.0.0.
13
13
 
14
+ > **Breaking change in 0.12.0**: When the persistent cache is enabled (`cache: true`), this plugin must now run **before** `metalsmith-static-files` in the pipeline, not after. The plugin writes variants to the source tree and the static-files plugin copies them to the build. See [Usage](#usage) for details.
15
+
14
16
  ## Features
15
17
 
16
18
  - **Multiple image formats**: Generates AVIF and WebP variants with JPEG/PNG fallbacks
17
19
  - **Responsive sizes**: Creates different image sizes for various device widths
20
+ - **Persistent cache**: Writes variants to a source-tree directory so subsequent builds (and CI) skip Sharp entirely
18
21
  - **Background image support**: Automatically processes unused images for CSS `image-set()` backgrounds
19
22
  - **Progressive loading**: Optional progressive image loading with low-quality placeholders
20
23
  - **Lazy loading**: Uses native browser lazy loading
@@ -35,45 +38,121 @@ npm install metalsmith-optimize-images
35
38
 
36
39
  ## Usage
37
40
 
38
- > This plugin **must** be run after assets are copied but before any final HTML processing.
41
+ When the persistent cache is enabled the plugin should run **before** the static-files copy so that newly generated variants land in the cache directory and get picked up by the copy step. When the cache is disabled the plugin should run **after** assets are copied, since it needs the images to already be in the Metalsmith files object.
42
+
43
+ ### With persistent cache (recommended)
39
44
 
40
45
  ```javascript
41
46
  metalsmith
47
+ .use(
48
+ optimizeImages({
49
+ cache: true,
50
+ widths: [320, 640, 960, 1280, 1920],
51
+ formats: ['avif', 'webp', 'original']
52
+
53
+ })
54
+ )
42
55
  .use(
43
56
  assets({
44
- source: 'lib/assets/', // Where to find assets
45
- destination: 'assets/' // Where to copy assets
57
+ source: 'lib/assets/',
58
+ destination: 'assets/'
59
+ })
60
+ );
61
+ ```
62
+
63
+ ### Without cache (original behaviour)
64
+
65
+ ```javascript
66
+ metalsmith
67
+ .use(
68
+ assets({
69
+ source: 'lib/assets/',
70
+ destination: 'assets/'
46
71
  })
47
72
  )
48
73
  .use(
49
74
  optimizeImages({
50
- // configuration options
51
75
  widths: [320, 640, 960, 1280, 1920],
52
76
  formats: ['avif', 'webp', 'original']
53
77
  })
54
78
  );
55
79
  ```
56
80
 
81
+ ## Theory of Operation
82
+
83
+ Understanding the full lifecycle helps explain why the plugin is structured the way it is and how the cache eliminates redundant work.
84
+
85
+ ### The problem
86
+
87
+ Sharp-based image processing is expensive. A typical site with 20 source images, five responsive widths, and three output formats generates 300 variant files. On a cold build this can take 30 seconds or more. On a CI host like Netlify the build starts from a clean checkout every time, so without intervention that cost is paid on every deploy even when no images changed.
88
+
89
+ ### How the cache solves it
90
+
91
+ The plugin can persist generated variants into a directory inside the source tree, for example `lib/assets/images/responsive/`. Because this directory is committed to git, CI clones already contain every variant that was generated on a previous build. The plugin checks the cache before calling Sharp: if a variant file already exists it is read from disk instead. Only genuinely new or changed images trigger Sharp processing.
92
+
93
+ ### Build pipeline flow
94
+
95
+ The cache changes where the plugin sits in the Metalsmith pipeline. Without the cache the plugin runs after the static-files copy because it needs images to be in the Metalsmith files object. With the cache enabled the plugin runs **before** the static-files copy:
96
+
97
+ ```
98
+ Source images on disk (lib/assets/images/)
99
+
100
+
101
+ ┌──────────────────────┐
102
+ │ optimize-images │ Reads source images from disk via sourcePrefix.
103
+ │ (runs first) │ Checks cache dir for existing variants.
104
+ │ │ Generates missing variants with Sharp.
105
+ │ │ Writes new variants to cache dir.
106
+ │ │ Rewrites HTML: <img> → <picture>.
107
+ │ │ Does NOT add variants to files object.
108
+ └──────────────────────┘
109
+
110
+
111
+ ┌──────────────────────┐
112
+ │ metalsmith-static │ Copies lib/assets/ → build/assets/.
113
+ │ (runs second) │ This includes the responsive/ cache dir,
114
+ │ │ so all variants end up in the build.
115
+ └──────────────────────┘
116
+
117
+
118
+ Final build output
119
+ ```
120
+
121
+ ### Source image discovery
122
+
123
+ When the plugin runs before the static-files copy, source images are not yet in the Metalsmith files object. The plugin derives a `sourcePrefix` from the cache path to locate them on disk. For example, if `cache` resolves to `lib/assets/images/responsive` and `outputDir` is `assets/images/responsive`, the prefix is `lib/`. An HTML reference to `assets/images/hero.jpg` maps to `lib/assets/images/hero.jpg` on disk.
124
+
125
+ ### Cache invalidation
126
+
127
+ HTML images include a content hash in their filenames (e.g., `hero-640w-a1b2c3d4.webp`). When a source image changes, its hash changes, the expected filename differs from anything on disk, and the cache misses naturally. Old variants with the previous hash remain in the cache directory but are harmless — they simply stop being referenced in HTML.
128
+
129
+ Background images use deterministic filenames without hashes (e.g., `hero-960w.webp`) for easier CSS authoring. This means the cache cannot detect content changes for background images automatically. If a background source image changes content without changing its filename, delete the cache directory to force regeneration.
130
+
131
+ ### What gets committed to git
132
+
133
+ The cache directory (e.g., `lib/assets/images/responsive/`) should be committed to the repository. It contains only generated variant files — binary images that are a deterministic function of the source images and plugin configuration. Committing them trades repository size for build speed. A typical site adds 50-100 MB to the repo but saves 30+ seconds on every CI build.
134
+
57
135
  ## Options
58
136
 
59
- | Option | Type | Default | Description |
60
- | --------------------- | ---------- | ------------------------------------- | ------------------------------------------------------------------------------ |
61
- | `widths` | `number[]` | `[320, 640, 960, 1280, 1920]` | Image sizes to generate |
62
- | `formats` | `string[]` | `['avif', 'webp', 'original']` | Image formats in order of preference |
63
- | `formatOptions` | `object` | See below | Format-specific compression settings |
64
- | `htmlPattern` | `string` | `**/*.html` | Glob pattern to match HTML files |
65
- | `imgSelector` | `string` | `img:not([data-no-responsive])` | CSS selector for images to process |
66
- | `outputDir` | `string` | `assets/images/responsive` | Where to store the responsive images |
67
- | `outputPattern` | `string` | `[filename]-[width]w-[hash].[format]` | Filename pattern with tokens |
68
- | `skipLarger` | `boolean` | `true` | Don't upscale images |
69
- | `lazy` | `boolean` | `true` | Use native lazy loading |
70
- | `dimensionAttributes` | `boolean` | `true` | Add width/height to prevent layout shift |
71
- | `sizes` | `string` | `(max-width: 768px) 100vw, 75vw` | Default sizes attribute |
72
- | `concurrency` | `number` | `5` | Process N images at a time |
73
- | `generateMetadata` | `boolean` | `false` | Generate a metadata JSON file at `{outputDir}/responsive-images-manifest.json` |
74
- | `isProgressive` | `boolean` | `false` | Enable progressive image loading |
75
- | `placeholder` | `object` | See below | Placeholder image settings |
76
- | `processUnusedImages` | `boolean` | `true` | Process unused images for background use |
137
+ | Option | Type | Default | Description |
138
+ | --------------------- | ------------------ | ------------------------------------- | ------------------------------------------------------------------------------ |
139
+ | `cache` | `boolean\|string` | `false` | Persistent cache. `true` uses `lib/<outputDir>`, string sets a custom path |
140
+ | `widths` | `number[]` | `[320, 640, 960, 1280, 1920]` | Image sizes to generate |
141
+ | `formats` | `string[]` | `['avif', 'webp', 'original']` | Image formats in order of preference |
142
+ | `formatOptions` | `object` | See below | Format-specific compression settings |
143
+ | `htmlPattern` | `string` | `**/*.html` | Glob pattern to match HTML files |
144
+ | `imgSelector` | `string` | `img:not([data-no-responsive])` | CSS selector for images to process |
145
+ | `outputDir` | `string` | `assets/images/responsive` | Where to store the responsive images |
146
+ | `outputPattern` | `string` | `[filename]-[width]w-[hash].[format]` | Filename pattern with tokens |
147
+ | `skipLarger` | `boolean` | `true` | Don't upscale images |
148
+ | `lazy` | `boolean` | `true` | Use native lazy loading |
149
+ | `dimensionAttributes` | `boolean` | `true` | Add width/height to prevent layout shift |
150
+ | `sizes` | `string` | `(max-width: 768px) 100vw, 75vw` | Default sizes attribute |
151
+ | `concurrency` | `number` | `5` | Process N images at a time |
152
+ | `generateMetadata` | `boolean` | `false` | Generate a metadata JSON file at `{outputDir}/responsive-images-manifest.json` |
153
+ | `isProgressive` | `boolean` | `false` | Enable progressive image loading |
154
+ | `placeholder` | `object` | See below | Placeholder image settings |
155
+ | `processUnusedImages` | `boolean` | `true` | Process unused images for background use |
77
156
 
78
157
  ### Default Format Options
79
158
 
@@ -104,9 +183,9 @@ The plugin operates in two phases:
104
183
 
105
184
  **Phase 1: HTML-Referenced Images**
106
185
 
107
- 1. Scans HTML files for image tags
108
- 2. Processes each image to create multiple sizes and formats
109
- 3. Creates a content hash for each image for efficient caching
186
+ 1. Scans HTML files for image tags matching the configured selector
187
+ 2. Processes each image to create multiple sizes and formats using Sharp
188
+ 3. Creates a content hash for each image for cache-busting filenames
110
189
  4. Replaces `<img>` tags with responsive `<picture>` elements
111
190
  5. Adds width/height attributes to prevent layout shifts
112
191
  6. Implements native lazy loading for better performance
@@ -116,7 +195,8 @@ The plugin operates in two phases:
116
195
  1. Finds images that weren't processed in Phase 1
117
196
  2. Generates 1x/2x variants (half size and original size) for retina displays
118
197
  3. Creates all configured formats (AVIF, WebP, original)
119
- 4. Suitable for use with CSS `image-set()` for background images
198
+ 4. Uses deterministic filenames without hashes for easier CSS authoring
199
+ 5. Suitable for use with CSS `image-set()` for background images
120
200
 
121
201
  ### Progressive Mode (experimental)
122
202
 
@@ -138,6 +218,28 @@ When `isProgressive: true` is enabled:
138
218
  metalsmith.use(optimizeImages());
139
219
  ```
140
220
 
221
+ ### With persistent cache
222
+
223
+ ```javascript
224
+ metalsmith.use(
225
+ optimizeImages({
226
+ cache: true
227
+ })
228
+ );
229
+ ```
230
+
231
+ This writes variants to `lib/assets/images/responsive/` (derived from `lib/` + the default `outputDir`). Commit this directory to git so CI builds skip Sharp entirely.
232
+
233
+ ### Custom cache path
234
+
235
+ ```javascript
236
+ metalsmith.use(
237
+ optimizeImages({
238
+ cache: 'lib/assets/images/responsive'
239
+ })
240
+ );
241
+ ```
242
+
141
243
  ### Custom configuration
142
244
 
143
245
  ```javascript
@@ -203,28 +305,19 @@ Add the `data-no-responsive` attribute to any image you don't want processed:
203
305
 
204
306
  The plugin automatically processes raster images and skips vector graphics:
205
307
 
206
- ### Processed
308
+ ### Processed
207
309
  - **JPEG** (`.jpg`, `.jpeg`)
208
310
  - **PNG** (`.png`)
209
311
  - **GIF** (`.gif`)
210
312
  - **WebP** (`.webp`)
211
313
  - **AVIF** (`.avif`)
212
314
 
213
- ### Automatically Skipped
214
- - **SVG** (`.svg`) - Vector graphics don't need responsive raster variants
315
+ ### Automatically Skipped
316
+ - **SVG** (`.svg`) vector graphics that scale perfectly at any resolution
215
317
  - **External URLs** (http/https)
216
318
  - **Data URLs** (`data:image/...`)
217
319
  - **Images with `data-no-responsive` attribute**
218
320
 
219
- ### Why SVGs are skipped
220
-
221
- SVG files are vector graphics that scale perfectly at any resolution without quality loss. Creating multiple raster sizes and formats from SVGs would:
222
- - Increase build time unnecessarily
223
- - Generate larger file sizes than the original vector
224
- - Lose the scalability benefits of SVG format
225
-
226
- If you have SVG logos or icons, they will remain untouched and work perfectly as-is.
227
-
228
321
  ## Background Images
229
322
 
230
323
  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`).
@@ -233,13 +326,13 @@ The plugin automatically processes images that aren't referenced in HTML for use
233
326
 
234
327
  After processing HTML-referenced images, the plugin:
235
328
 
236
- 1. **Scans the Metalsmith files object** for all images
237
- 2. **Excludes already-processed images** (those found during HTML scanning)
238
- 3. **Excludes responsive variants** (generated images in the outputDir)
239
- 4. **Generates 1x/2x variants** using actual image dimensions:
329
+ 1. Scans the Metalsmith files object and filesystem for all images
330
+ 2. Excludes already-processed images (those found during HTML scanning)
331
+ 3. Excludes responsive variants (generated images in the outputDir)
332
+ 4. Generates 1x/2x variants using actual image dimensions:
240
333
  - **1x variant**: Half the original size for regular displays
241
334
  - **2x variant**: Original image size for retina displays (sharper on high-DPI screens)
242
- 5. **Creates all formats** (AVIF, WebP, original) for optimal browser support
335
+ 5. Creates all formats (AVIF, WebP, original) for optimal browser support
243
336
 
244
337
  ### Using Background Images with CSS
245
338
 
@@ -254,7 +347,7 @@ assets/images/responsive/hero-960w.jpg (1x - half 960px width for regular dis
254
347
  assets/images/responsive/hero-1920w.jpg (2x - original 1920px width, sharper on retina)
255
348
  ```
256
349
 
257
- **Note**: Background images are generated **without hashes** for easier CSS authoring. HTML images still include hashes for cache-busting.
350
+ Background images are generated without hashes for easier CSS authoring. HTML images still include hashes for cache-busting.
258
351
 
259
352
  Use them in CSS with `image-set()`:
260
353
 
@@ -278,36 +371,24 @@ Use them in CSS with `image-set()`:
278
371
  ```javascript
279
372
  metalsmith.use(
280
373
  optimizeImages({
281
- // Standard HTML image processing
282
374
  widths: [320, 640, 960, 1280, 1920],
283
375
  formats: ['avif', 'webp', 'original'],
284
-
285
- // Background image processing
286
- processUnusedImages: true, // Enable background processing
287
-
288
- // Generate metadata to see all variants
376
+ processUnusedImages: true,
289
377
  generateMetadata: true
290
378
  })
291
379
  );
292
380
  ```
293
381
 
294
- ### Benefits of Background Image Processing
295
-
296
- - **Automatic format optimization** - Browser selects best supported format
297
- - **Retina display support** - 2x variants provide crisp images on high-DPI screens
298
- - **No manual work** - Plugin automatically finds and processes unused images in Metalsmith files object
299
- - **Consistent workflow** - Same formats and quality settings as HTML images
300
-
301
382
  ## Progressive Loading
302
383
 
303
384
  ### Overview
304
385
 
305
386
  Progressive loading provides a smooth user experience by:
306
387
 
307
- 1. **Immediate display**: Shows a low-quality placeholder instantly
308
- 2. **Smooth transitions**: Fades from placeholder to high-quality image
309
- 3. **Lazy loading**: Only loads high-resolution images when they enter the viewport
310
- 4. **Format optimization**: Automatically serves the best supported format
388
+ 1. Showing a low-quality placeholder instantly
389
+ 2. Fading from placeholder to high-quality image
390
+ 3. Only loading high-resolution images when they enter the viewport
391
+ 4. Automatically serving the best supported format
311
392
 
312
393
  ### Implementation
313
394
 
@@ -448,6 +529,10 @@ metalsmith.env('DEBUG', 'metalsmith-optimize-images*');
448
529
  }
449
530
  ```
450
531
 
532
+ ## Test Coverage
533
+
534
+ 88 tests covering all major functionality including unit tests for utilities, integration tests with real Metalsmith instances, cache persistence tests, and edge case coverage.
535
+
451
536
  ## License
452
537
 
453
538
  MIT
@@ -468,6 +553,6 @@ All AI-assisted code has been reviewed and tested to ensure it meets project sta
468
553
  [metalsmith-url]: https://metalsmith.io
469
554
  [license-badge]: https://img.shields.io/github/license/wernerglinka/metalsmith-optimize-images
470
555
  [license-url]: LICENSE
471
- [coverage-badge]: https://img.shields.io/badge/test%20coverage-95%25-brightgreen
556
+ [coverage-badge]: https://img.shields.io/badge/test%20coverage-92%25-brightgreen
472
557
  [coverage-url]: https://github.com/wernerglinka/metalsmith-optimize-images/actions/workflows/test.yml
473
558
  [modules-badge]: https://img.shields.io/badge/modules-ESM%2FCJS-blue