Skip to content

Commit 17fff6b

Browse files
authored
feat: CNAME & Private CDN (#71)
# Description Adds the ability to enable Private CDN or specify a custom CNAME for Advanced Plan users. ## Issue Ticket Number <!-- Specifiy which issue this fixes by referencing the issue number (`#11`) or issue URL. --> <!-- Example: Fixes #1 --> Fixes #51 ## 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 ce3d922 commit 17fff6b

File tree

8 files changed

+185
-29
lines changed

8 files changed

+185
-29
lines changed

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,11 +89,13 @@ npm install netlify-plugin-cloudinary
8989
| Name | Required | Example | Description |
9090
|-----------------|----------|-----------| ------------|
9191
| cloudName | No* | mycloud | Cloudinary Cloud Name |
92+
| cname | No | domain.com | The custom domain name (CNAME) to use for building URLs (Advanced Plan Users) |
9293
| deliveryType | No | fetch | The method by which Cloudinary stores and delivers images (Ex: fetch, upload) |
93-
| imagesPath | No | /assets | Local path application serves image assets from |
9494
| folder | No | myfolder | Folder all media will be stored in. Defaults to Netlify site name |
95-
| uploadPreset | No | my-preset | Defined set of asset upload defaults in Cloudinary |
95+
| imagesPath | No | /assets | Local path application serves image assets from |
9696
| loadingStrategy | No | eager | The method in which in which images are loaded (Ex: lazy, eager) |
97+
| privateCdn | No | true | Enables Private CDN Distribution (Advanced Plan Users) |
98+
| uploadPreset | No | my-preset | Defined set of asset upload defaults in Cloudinary |
9799

98100
*Cloud Name must be set as an environment variable if not as an input
99101

docs/src/pages/configuration.mdx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,12 @@ import OgImage from '../components/OgImage';
2222
| Name | Required | Example | Description |
2323
|-----------------|----------|-----------| ------------|
2424
| cloudName | No* | mycloud | Cloudinary Cloud Name |
25+
| cname | No | domain.com | The custom domain name (CNAME) to use for building URLs (Advanced Plan Users) |
2526
| deliveryType | No | fetch | The method by which Cloudinary stores and delivers images (Ex: fetch, upload) |
2627
| folder | No | myfolder | Folder all media will be stored in. Defaults to Netlify site name |
27-
| imagesPath | No | /assets | Local path application serves image assets from |
28+
| imagesPath | No | /assets | Local path application serves image assets from |
2829
| loadingStrategy | No | eager | The method in which in which images are loaded (Ex: lazy, eager) |
30+
| privateCdn | No | true | Enables Private CDN Distribution (Advanced Plan Users) |
2931
| uploadPreset | No | my-preset | Defined set of asset upload defaults in Cloudinary |
3032

3133
<Callout emoji={false}>

netlify-plugin-cloudinary/manifest.yml

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,27 @@ inputs:
33
- name: cloudName
44
required: false
55
description: "Cloudinary Cloud Name - can be used as an input or environment variable via CLOUDINARY_CLOUD_NAME"
6+
- name: cname
7+
required: false
8+
description: "The custom domain name (CNAME) to use for building URLs (Advanced Plan Users)"
69
- name: deliveryType
710
required: false
811
description: "The method in which Cloudinary stores and delivers images (Ex: fetch, upload)"
912
default: "fetch"
10-
- name: imagesPath
11-
required: false
12-
description: "Local path application serves image assets from"
13-
default: "/images"
1413
- name: folder
1514
required: false
1615
description: "Folder all media will be stored in. Defaults to Netlify site name"
17-
- name: uploadPreset
16+
- name: imagesPath
1817
required: false
19-
description: "Defined set of asset upload defaults in Cloudinary"
18+
description: "Local path application serves image assets from"
19+
default: "/images"
2020
- name: loadingStrategy
2121
required: false
2222
description: "The method in which in which images are loaded (Ex: lazy, eager)"
2323
default: "lazy"
24+
- name: privateCdn
25+
required: false
26+
description: "Enable Private CDN Distribution (Advanced Plan Users)"
27+
- name: uploadPreset
28+
required: false
29+
description: "Defined set of asset upload defaults in Cloudinary"

netlify-plugin-cloudinary/src/index.ts

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,11 +64,13 @@ type Constants = {
6464
*/
6565
type Inputs = {
6666
cloudName: string;
67+
cname: string;
6768
deliveryType: string;
68-
imagesPath: string;
6969
folder: string;
70-
uploadPreset: string;
70+
imagesPath: string;
7171
loadingStrategy: string;
72+
privateCdn: boolean;
73+
uploadPreset: string;
7274
};
7375

7476
type Utils = {
@@ -130,12 +132,14 @@ export async function onBuild({
130132
const { PUBLISH_DIR } = constants;
131133

132134
const {
135+
cname,
133136
deliveryType,
134-
uploadPreset,
135137
folder = process.env.SITE_NAME,
136138
imagesPath = CLOUDINARY_ASSET_DIRECTORIES.find(
137139
({ inputKey }) => inputKey === 'imagesPath',
138140
)?.path,
141+
privateCdn,
142+
uploadPreset,
139143
} = inputs;
140144

141145
if (!folder) {
@@ -159,9 +163,14 @@ export async function onBuild({
159163
}
160164

161165
configureCloudinary({
166+
// Base credentials
162167
cloudName,
163168
apiKey,
164169
apiSecret,
170+
171+
// Configuration
172+
cname,
173+
privateCdn,
165174
});
166175

167176
// Look for any available images in the provided imagesPath to collect
@@ -238,6 +247,7 @@ export async function onBuild({
238247
CLOUDINARY_ASSET_DIRECTORIES.map(
239248
async ({ inputKey, path: defaultPath }) => {
240249
const mediaPath = inputs[inputKey as keyof Inputs] || defaultPath;
250+
// @ts-ignore Unsure how to type the above so that Inputs['privateCdn'] doesnt mess up types here
241251
const cldAssetPath = `/${path.join(PUBLIC_ASSET_PATH, mediaPath)}`;
242252
const cldAssetUrl = `${host}${cldAssetPath}`;
243253

@@ -287,7 +297,13 @@ export async function onPostBuild({
287297
console.log(`[Cloudinary] Using host: ${host}`);
288298

289299
const { PUBLISH_DIR } = constants;
290-
const { deliveryType, uploadPreset, folder = process.env.SITE_NAME } = inputs;
300+
const {
301+
cname,
302+
deliveryType,
303+
folder = process.env.SITE_NAME,
304+
privateCdn,
305+
uploadPreset,
306+
} = inputs;
291307

292308
if (!folder) {
293309
utils.build.failPlugin(ERROR_SITE_NAME_REQUIRED);
@@ -304,9 +320,14 @@ export async function onPostBuild({
304320
}
305321

306322
configureCloudinary({
323+
// Base credentials
307324
cloudName,
308325
apiKey,
309326
apiSecret,
327+
328+
// Configuration
329+
cname,
330+
privateCdn,
310331
});
311332

312333
// Find all HTML source files in the publish directory

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

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,17 @@ 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 } from 'cloudinary'
5+
import { v2 as cloudinary, ConfigOptions } from 'cloudinary'
66

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

1010
type CloudinaryConfig = {
11+
apiKey?: string;
12+
apiSecret?: string;
1113
cloudName: string;
12-
apiKey: string;
13-
apiSecret: string;
14+
cname?: string;
15+
privateCdn?: boolean;
1416
}
1517
type DeliveryType =
1618
string
@@ -81,11 +83,24 @@ export function getCloudinary(config: CloudinaryOptions & CloudinaryConfig) {
8183
* configureCloudinary
8284
*/
8385
export function configureCloudinary(config: CloudinaryConfig) {
84-
cloudinary.config({
86+
const cloudinaryConfig: ConfigOptions = {
8587
cloud_name: config.cloudName,
8688
api_key: config.apiKey,
8789
api_secret: config.apiSecret,
88-
})
90+
private_cdn: config.privateCdn,
91+
secure: true
92+
}
93+
94+
if ( config.cname ) {
95+
cloudinaryConfig.secure_distribution = config.cname;
96+
// When configuring a cname, we need to additionally set private CDN
97+
// to be true in order to work properly, which may not be obvious
98+
// to those setting it up
99+
cloudinaryConfig.private_cdn = true;
100+
}
101+
102+
cloudinary.config(cloudinaryConfig);
103+
89104
return cloudinary
90105
}
91106

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
const { configureCloudinary, getCloudinaryUrl, updateHtmlImagesToCloudinary } = require('../../src/lib/cloudinary');
2+
3+
describe('lib/util', () => {
4+
const ENV_ORIGINAL = process.env;
5+
const cname = 'spacejelly.dev';
6+
7+
beforeEach(() => {
8+
jest.resetModules();
9+
10+
process.env = { ...ENV_ORIGINAL };
11+
process.env.CLOUDINARY_CLOUD_NAME = 'testcloud';
12+
process.env.CLOUDINARY_API_KEY = '123456789012345';
13+
process.env.CLOUDINARY_API_SECRET = 'abcd1234';
14+
15+
configureCloudinary({
16+
cloudName: process.env.CLOUDINARY_CLOUD_NAME,
17+
apiKey: process.env.CLOUDINARY_API_KEY,
18+
apiSecret: process.env.CLOUDINARY_API_SECRET,
19+
cname,
20+
});
21+
});
22+
23+
afterAll(() => {
24+
process.env = ENV_ORIGINAL;
25+
});
26+
27+
describe('getCloudinaryUrl', () => {
28+
29+
test('should create a Cloudinary URL with delivery type of fetch from a local image', async () => {
30+
const { cloudinaryUrl } = await getCloudinaryUrl({
31+
deliveryType: 'fetch',
32+
path: '/images/stranger-things-dustin.jpeg',
33+
localDir: '/tests/images',
34+
remoteHost: 'https://cloudinary.netlify.app'
35+
});
36+
37+
expect(cloudinaryUrl).toEqual(`https://${cname}/image/fetch/f_auto,q_auto/https://cloudinary.netlify.app/images/stranger-things-dustin.jpeg`);
38+
});
39+
40+
});
41+
42+
describe('updateHtmlImagesToCloudinary', () => {
43+
44+
it('should replace a local image with a Cloudinary URL', async () => {
45+
const sourceHtml = '<html><head></head><body><p><img src="/images/stranger-things-dustin.jpeg"></p></body></html>';
46+
47+
const { html } = await updateHtmlImagesToCloudinary(sourceHtml, {
48+
deliveryType: 'fetch',
49+
localDir: 'tests',
50+
remoteHost: 'https://cloudinary.netlify.app'
51+
});
52+
53+
expect(html).toEqual(`<html><head></head><body><p><img src=\"https://${cname}/image/fetch/f_auto,q_auto/https://cloudinary.netlify.app/images/stranger-things-dustin.jpeg\" loading=\"lazy"\></p></body></html>`);
54+
});
55+
56+
});
57+
58+
});
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
const { configureCloudinary, getCloudinaryUrl, updateHtmlImagesToCloudinary } = require('../../src/lib/cloudinary');
2+
3+
describe('lib/util', () => {
4+
const ENV_ORIGINAL = process.env;
5+
6+
beforeEach(() => {
7+
jest.resetModules();
8+
9+
process.env = { ...ENV_ORIGINAL };
10+
process.env.CLOUDINARY_CLOUD_NAME = 'testcloud';
11+
process.env.CLOUDINARY_API_KEY = '123456789012345';
12+
process.env.CLOUDINARY_API_SECRET = 'abcd1234';
13+
14+
configureCloudinary({
15+
cloudName: process.env.CLOUDINARY_CLOUD_NAME,
16+
apiKey: process.env.CLOUDINARY_API_KEY,
17+
apiSecret: process.env.CLOUDINARY_API_SECRET,
18+
privateCdn: true,
19+
});
20+
});
21+
22+
afterAll(() => {
23+
process.env = ENV_ORIGINAL;
24+
});
25+
26+
describe('getCloudinaryUrl', () => {
27+
28+
test('should create a Cloudinary URL with delivery type of fetch from a local image', async () => {
29+
const { cloudinaryUrl } = await getCloudinaryUrl({
30+
deliveryType: 'fetch',
31+
path: '/images/stranger-things-dustin.jpeg',
32+
localDir: '/tests/images',
33+
remoteHost: 'https://cloudinary.netlify.app'
34+
});
35+
36+
expect(cloudinaryUrl).toEqual(`https://${process.env.CLOUDINARY_CLOUD_NAME}-res.cloudinary.com/image/fetch/f_auto,q_auto/https://cloudinary.netlify.app/images/stranger-things-dustin.jpeg`);
37+
});
38+
39+
});
40+
41+
describe('updateHtmlImagesToCloudinary', () => {
42+
43+
it('should replace a local image with a Cloudinary URL', async () => {
44+
const sourceHtml = '<html><head></head><body><p><img src="/images/stranger-things-dustin.jpeg"></p></body></html>';
45+
46+
const { html } = await updateHtmlImagesToCloudinary(sourceHtml, {
47+
deliveryType: 'fetch',
48+
localDir: 'tests',
49+
remoteHost: 'https://cloudinary.netlify.app'
50+
});
51+
52+
expect(html).toEqual(`<html><head></head><body><p><img src=\"https://${process.env.CLOUDINARY_CLOUD_NAME}-res.cloudinary.com/image/fetch/f_auto,q_auto/https://cloudinary.netlify.app/images/stranger-things-dustin.jpeg\" loading=\"lazy"\></p></body></html>`);
53+
});
54+
55+
});
56+
57+
});

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

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,9 @@
1-
const { getCloudinary, createPublicId, getCloudinaryUrl, updateHtmlImagesToCloudinary } = require('../../src/lib/cloudinary');
1+
const { getCloudinary, getCloudinaryUrl, createPublicId, configureCloudinary, updateHtmlImagesToCloudinary } = require('../../src/lib/cloudinary');
22

33
const mockDemo = require('../mocks/demo.json');
44

55
const cloudinary = getCloudinary();
66

7-
8-
const CLOUDINARY_CLOUD_NAME = process.env.CLOUDINARY_CLOUD_NAME;
9-
const CLOUDINARY_API_KEY = process.env.CLOUDINARY_API_KEY;
10-
const CLOUDINARY_API_SECRET = process.env.CLOUDINARY_API_SECRET;
11-
127
describe('lib/util', () => {
138
const ENV_ORIGINAL = process.env;
149

@@ -20,11 +15,11 @@ describe('lib/util', () => {
2015
process.env.CLOUDINARY_API_KEY = '123456789012345';
2116
process.env.CLOUDINARY_API_SECRET = 'abcd1234';
2217

23-
cloudinary.config({
24-
cloud_name: CLOUDINARY_CLOUD_NAME,
25-
api_key: CLOUDINARY_API_KEY,
26-
api_secret: CLOUDINARY_API_SECRET
27-
});
18+
configureCloudinary({
19+
cloudName: process.env.CLOUDINARY_CLOUD_NAME,
20+
apiKey: process.env.CLOUDINARY_API_KEY,
21+
apiSecret: process.env.CLOUDINARY_API_SECRET,
22+
})
2823
});
2924

3025
afterAll(() => {

0 commit comments

Comments
 (0)