Skip to content

Commit 57cb8fb

Browse files
authored
feat: Max Size (#78)
# Description Allows the ability to pass in the maxSize option where you can configure a maximum width and height to use for images. Images will automatically resize down to the maximum values preserving the aspect ratio. It will also not upscale, only downscale. ``` [plugins.inputs.maxSize] width = 1200 height = 800 ``` ## Issue Ticket Number <!-- Specifiy which issue this fixes by referencing the issue number (`#11`) or issue URL. --> <!-- Example: Fixes #1 --> Fixes #62 ## Type of change <!-- Please select all options that are applicable. --> - [ ] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) - [ ] This change requires a documentation update # Checklist <!-- These must all be followed and checked. --> - [ ] I have followed the contributing guidelines of this project as mentioned in [CONTRIBUTING.md](/CONTRIBUTING.md) - [ ] I have created an [issue](https://github.com/colbyfayock/netlify-plugin-cloudinary/issues) ticket for this PR - [ ] I have checked to ensure there aren't other open [Pull Requests](https://github.com/colbyfayock/netlify-plugin-cloudinary/pulls) for the same update/change? - [ ] I have performed a self-review of my own code - [ ] I have run tests locally to ensure they all pass - [ ] I have commented my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes needed to the documentation
1 parent 7b85b1e commit 57cb8fb

File tree

11 files changed

+305
-18
lines changed

11 files changed

+305
-18
lines changed

README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,11 +94,30 @@ npm install netlify-plugin-cloudinary
9494
| folder | string | No | myfolder | Folder all media will be stored in. Defaults to Netlify site name |
9595
| imagesPath | string/Array | No | /assets | Local path application serves image assets from |
9696
| loadingStrategy | string | No | eager | The method in which in which images are loaded (Ex: lazy, eager) |
97+
| maxSize | object | No | eager | See Below. |
9798
| privateCdn | boolean | No | true | Enables Private CDN Distribution (Advanced Plan Users) |
9899
| uploadPreset | string | No | my-preset | Defined set of asset upload defaults in Cloudinary |
99100

100101
*Cloud Name must be set as an environment variable if not as an input
101102

103+
#### Max Size
104+
105+
The Max Size option gives you the ability to configure a maximum width and height that images will scale down to, helping to avoid serving unnecessarily large images.
106+
107+
By default, the aspect ratio of the images are preserved, so by specifying both a maximum width and height, you're telling Cloudinary to scale the image down so that neither the width or height are beyond that value.
108+
109+
Additionally, the plugin uses a crop method of `limit` which avoids upscaling images if the images are already smaller than the given size, which reduces unnecessary upscaling as the browser will typically automatically handle.
110+
111+
The options available are:
112+
113+
| Name | Type | Example | Description |
114+
|-----------------|---------|-----------| ------------|
115+
| dpr | string | 2.0 | Device Pixel Ratio which essentially multiplies the width and height for pixel density. |
116+
| height | number | 600 | Maximum height an image can be delivered as. |
117+
| width | number | 800 | Maximum width an image can be delivered as. |
118+
119+
It's important to note that this will not change the width or height attribute of the image within the DOM, this will only be the image that is being delivered by Cloudinary.
120+
102121
### Environment Variables
103122

104123
| Name | Required | Example | Description |

docs/src/pages/configuration.mdx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,32 @@ import OgImage from '../components/OgImage';
2727
| folder | string | No | myfolder | Folder all media will be stored in. Defaults to Netlify site name |
2828
| imagesPath | string/Array | No | /assets | Local path application serves image assets from |
2929
| loadingStrategy | string | No | eager | The method in which in which images are loaded (Ex: lazy, eager) |
30+
| maxSize | object | No | eager | See Below. |
3031
| privateCdn | boolean | No | true | Enables Private CDN Distribution (Advanced Plan Users) |
3132
| uploadPreset | string | No | my-preset | Defined set of asset upload defaults in Cloudinary |
3233

3334
<Callout emoji={false}>
3435
Cloud Name must be set as an environment variable if not as an input
3536
</Callout>
3637

38+
### Max Size
39+
40+
The Max Size option gives you the ability to configure a maximum width and height that images will scale down to, helping to avoid serving unnecessarily large images.
41+
42+
By default, the aspect ratio of the images are preserved, so by specifying both a maximum width and height, you're telling Cloudinary to scale the image down so that neither the width or height are beyond that value.
43+
44+
Additionally, the plugin uses a crop method of `limit` which avoids upscaling images if the images are already smaller than the given size, which reduces unnecessary upscaling as the browser will typically automatically handle.
45+
46+
The options available are:
47+
48+
| Name | Type | Example | Description |
49+
|-----------------|---------|-----------| ------------|
50+
| dpr | string | 2.0 | Device Pixel Ratio which essentially multiplies the width and height for pixel density. |
51+
| height | number | 600 | Maximum height an image can be delivered as. |
52+
| width | number | 800 | Maximum width an image can be delivered as. |
53+
54+
It's important to note that this will not change the width or height attribute of the image within the DOM, this will only be the image that is being delivered by Cloudinary.
55+
3756
## Environment Variables
3857

3958
| Name | Required | Example | Description |

netlify-plugin-cloudinary/manifest.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ inputs:
2121
required: false
2222
description: "The method in which in which images are loaded (Ex: lazy, eager)"
2323
default: "lazy"
24+
- name: maxSize
25+
required: false
26+
description: "Maximum dimensions (width and height) for an image to be delivered"
2427
- name: privateCdn
2528
required: false
2629
description: "Enable Private CDN Distribution (Advanced Plan Users)"

netlify-plugin-cloudinary/src/index.ts

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,14 @@ import fs from 'node:fs/promises';
22
import path from 'node:path';
33
import { glob } from 'glob';
44

5+
import { Inputs } from './types/integration';
6+
57
import {
68
configureCloudinary,
79
updateHtmlImagesToCloudinary,
810
getCloudinaryUrl,
911
Assets,
12+
getTransformationsFromInputs
1013
} from './lib/cloudinary';
1114
import { findAssetsByPath } from './lib/util';
1215

@@ -62,20 +65,7 @@ type Constants = {
6265
SITE_ID: string;
6366
};
6467

65-
/**
66-
* this type is built based on the content of the plugin manifest file
67-
* Information found here https://docs.netlify.com/integrations/build-plugins/create-plugins/#inputs
68-
*/
69-
type Inputs = {
70-
cloudName: string;
71-
cname: string;
72-
deliveryType: string;
73-
folder: string;
74-
imagesPath: string | Array<string>;
75-
loadingStrategy: string;
76-
privateCdn: boolean;
77-
uploadPreset: string;
78-
};
68+
7969

8070
type Utils = {
8171
build: {
@@ -143,6 +133,7 @@ export async function onBuild({
143133
imagesPath = CLOUDINARY_ASSET_DIRECTORIES.find(
144134
({ inputKey }) => inputKey === 'imagesPath',
145135
)?.path,
136+
maxSize,
146137
privateCdn,
147138
uploadPreset,
148139
} = inputs;
@@ -185,6 +176,8 @@ export async function onBuild({
185176
privateCdn,
186177
});
187178

179+
const transformations = getTransformationsFromInputs(inputs);
180+
188181
// Look for any available images in the provided imagesPath to collect
189182
// asset details and to grab a Cloudinary URL to use later
190183

@@ -216,6 +209,7 @@ export async function onBuild({
216209
localDir: PUBLISH_DIR,
217210
uploadPreset,
218211
remoteHost: host,
212+
transformations
219213
});
220214

221215
return {
@@ -283,6 +277,7 @@ export async function onBuild({
283277
folder,
284278
path: `${cldAssetUrl}/:splat`,
285279
uploadPreset,
280+
transformations
286281
});
287282

288283
netlifyConfig.redirects.unshift({
@@ -367,6 +362,8 @@ export async function onPostBuild({
367362
privateCdn,
368363
});
369364

365+
const transformations = getTransformationsFromInputs(inputs);
366+
370367
// Find all HTML source files in the publish directory
371368

372369
const pages = glob.sync(`${PUBLISH_DIR}/**/*.html`);
@@ -382,6 +379,7 @@ export async function onPostBuild({
382379
folder,
383380
localDir: PUBLISH_DIR,
384381
remoteHost: host,
382+
transformations
385383
});
386384

387385
await fs.writeFile(page, html);

netlify-plugin-cloudinary/src/lib/cloudinary.ts

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@ import crypto from 'crypto'
22
import path from 'path'
33
import fetch from 'node-fetch'
44
import { JSDOM } from 'jsdom'
5-
import { v2 as cloudinary, ConfigOptions } from 'cloudinary'
5+
import { v2 as cloudinary, ConfigOptions, TransformationOptions } from 'cloudinary'
66

77
import { isRemoteUrl, determineRemoteUrl } from './util'
88
import { ERROR_CLOUD_NAME_REQUIRED } from '../data/errors'
99

10+
import { Inputs } from '../types/integration';
11+
1012
type CloudinaryConfig = {
1113
apiKey?: string;
1214
apiSecret?: string;
@@ -52,11 +54,13 @@ type OtherDelivery = {
5254
deliveryType: Omit<DeliveryType, 'fetch'>;
5355
remoteHost?: string
5456
}
55-
type CloudinaryOptions = {
57+
58+
export type CloudinaryOptions = {
5659
folder: string,
5760
path: string;
5861
localDir?: string;
5962
uploadPreset: string;
63+
transformations?: Array<TransformationOptions>;
6064
} & (FetchDelivery | OtherDelivery)
6165

6266
export type Assets = {
@@ -139,6 +143,7 @@ export async function getCloudinaryUrl(options: CloudinaryOptions) {
139143
path: filePath,
140144
localDir,
141145
remoteHost,
146+
transformations = [],
142147
uploadPreset,
143148
} = options
144149

@@ -232,6 +237,7 @@ export async function getCloudinaryUrl(options: CloudinaryOptions) {
232237
fetch_format: 'auto',
233238
quality: 'auto',
234239
},
240+
...transformations
235241
],
236242
})
237243

@@ -267,6 +273,7 @@ export async function updateHtmlImagesToCloudinary(html: string, options: Update
267273
localDir,
268274
remoteHost,
269275
loadingStrategy = 'lazy',
276+
transformations
270277
} = options
271278

272279
const errors = []
@@ -292,7 +299,6 @@ export async function updateHtmlImagesToCloudinary(html: string, options: Update
292299

293300
// If we don't have an asset and thus don't have a Cloudinary URL, create
294301
// one for our asset
295-
296302
if (!cloudinaryUrl) {
297303
try {
298304
const { cloudinaryUrl: url } = await getCloudinaryUrl({
@@ -302,6 +308,7 @@ export async function updateHtmlImagesToCloudinary(html: string, options: Update
302308
localDir,
303309
uploadPreset,
304310
remoteHost,
311+
transformations
305312
})
306313
cloudinaryUrl = url
307314
} catch (e) {
@@ -372,3 +379,22 @@ export async function updateHtmlImagesToCloudinary(html: string, options: Update
372379
}
373380
}
374381

382+
/**
383+
* getTransformationsFromInputs
384+
*/
385+
386+
export function getTransformationsFromInputs(inputs: Inputs) {
387+
const { maxSize } = inputs;
388+
389+
const transformations: CloudinaryOptions['transformations'] = [];
390+
391+
if ( typeof maxSize === 'object' ) {
392+
transformations.push({
393+
height: maxSize.height,
394+
width: maxSize.width,
395+
crop: 'limit',
396+
dpr: maxSize.dpr
397+
})
398+
}
399+
return transformations;
400+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/**
2+
* this type is built based on the content of the plugin manifest file
3+
* Information found here https://docs.netlify.com/integrations/build-plugins/create-plugins/#inputs
4+
*/
5+
export type Inputs = {
6+
cloudName: string;
7+
cname: string;
8+
deliveryType: string;
9+
folder: string;
10+
imagesPath: string | Array<string>;
11+
loadingStrategy: string;
12+
maxSize: {
13+
dpr: number | string;
14+
height: number;
15+
width: number;
16+
};
17+
privateCdn: boolean;
18+
uploadPreset: string;
19+
};

netlify-plugin-cloudinary/tests/lib/cloudinary.test.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,24 @@ describe('lib/util', () => {
106106
// expect(cloudinaryUrl).toEqual(`https://res.cloudinary.com/${process.env.CLOUDINARY_CLOUD_NAME}/image/upload/f_auto,q_auto/vtYmp1x-ae71a79c9c36b8d5dba872c3b274a444`);
107107
// });
108108

109+
test('should apply transformations', async () => {
110+
const maxSize = {
111+
width: 800,
112+
height: 600,
113+
dpr: '3.0',
114+
crop: 'limit'
115+
}
116+
const { cloudinaryUrl } = await getCloudinaryUrl({
117+
deliveryType: 'fetch',
118+
path: '/images/stranger-things-dustin.jpeg',
119+
localDir: '/tests/images',
120+
remoteHost: 'https://cloudinary.netlify.app',
121+
transformations: [maxSize]
122+
});
123+
124+
expect(cloudinaryUrl).toEqual(`https://res.cloudinary.com/${process.env.CLOUDINARY_CLOUD_NAME}/image/fetch/f_auto,q_auto/c_${maxSize.crop},dpr_${maxSize.dpr},h_${maxSize.height},w_${maxSize.width}/https://cloudinary.netlify.app/images/stranger-things-dustin.jpeg`);
125+
});
126+
109127
});
110128

111129
describe('updateHtmlImagesToCloudinary', () => {

netlify-plugin-cloudinary/tests/mocks/html/test-0.html

Lines changed: 28 additions & 0 deletions
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)