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 +147 -62
- package/lib/index.cjs +355 -84
- package/lib/index.cjs.map +1 -1
- package/lib/index.js +355 -84
- package/lib/index.js.map +1 -1
- package/package.json +11 -13
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
|
-
|
|
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/',
|
|
45
|
-
destination: '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
|
|
60
|
-
| --------------------- |
|
|
61
|
-
| `
|
|
62
|
-
| `
|
|
63
|
-
| `
|
|
64
|
-
| `
|
|
65
|
-
| `
|
|
66
|
-
| `
|
|
67
|
-
| `
|
|
68
|
-
| `
|
|
69
|
-
| `
|
|
70
|
-
| `
|
|
71
|
-
| `
|
|
72
|
-
| `
|
|
73
|
-
| `
|
|
74
|
-
| `
|
|
75
|
-
| `
|
|
76
|
-
| `
|
|
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
|
|
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.
|
|
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
|
-
###
|
|
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
|
-
###
|
|
214
|
-
- **SVG** (`.svg`)
|
|
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.
|
|
237
|
-
2.
|
|
238
|
-
3.
|
|
239
|
-
4.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
308
|
-
2.
|
|
309
|
-
3.
|
|
310
|
-
4.
|
|
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-
|
|
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
|