Skip to content

Commit e0d1f03

Browse files
committed
setting up assets on prebuild to make available same data on both other steps, setting up upload-specific redirects for each asset
1 parent 93428d6 commit e0d1f03

File tree

5 files changed

+205
-82
lines changed

5 files changed

+205
-82
lines changed

netlify.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,5 @@
99
package = "."
1010

1111
[plugins.inputs]
12-
cloudName = "colbycloud"
12+
cloudName = "colbycloud"
13+
# deliveryType = "upload"

src/index.js

Lines changed: 121 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,7 @@ const CLOUDINARY_ASSET_DIRECTORIES = [
1919
*/
2020

2121
module.exports = {
22-
23-
async onBuild({ netlifyConfig, constants, inputs }) {
22+
async onPreBuild({ netlifyConfig, constants, inputs }) {
2423
const host = process.env.DEPLOY_PRIME_URL || process.env.NETLIFY_HOST;
2524

2625
if ( !host ) {
@@ -37,7 +36,16 @@ module.exports = {
3736
folder = process.env.SITE_NAME
3837
} = inputs;
3938

39+
// If we're using the fetch API, we don't need to worry about uploading any
40+
// of the media as itw ill all be publicly accessible, so we can skip this step
41+
42+
if ( deliveryType === 'fetch' ) {
43+
console.log('Skipping: Delivery type set to fetch.')
44+
return;
45+
}
46+
4047
const cloudName = process.env.CLOUDINARY_CLOUD_NAME || inputs.cloudName;
48+
4149
const apiKey = process.env.CLOUDINARY_API_KEY;
4250
const apiSecret = process.env.CLOUDINARY_API_SECRET;
4351

@@ -51,40 +59,126 @@ module.exports = {
5159
apiSecret
5260
});
5361

54-
await Promise.all(CLOUDINARY_ASSET_DIRECTORIES.map(async ({ name: mediaName, inputKey, path: defaultPath }) => {
55-
const mediaPath = inputs[inputKey] || defaultPath;
56-
const cldAssetPath = `/${path.join(PUBLIC_ASSET_PATH, mediaPath)}`;
57-
const cldAssetUrl = `${host}/${cldAssetPath}`;
62+
const imagesDirectory = glob.sync(`${PUBLISH_DIR}/images/**/*`);
63+
const imagesFiles = imagesDirectory.filter(file => !!path.extname(file));
5864

59-
const assetRedirectUrl = await getCloudinaryUrl({
60-
deliveryType: 'fetch',
61-
folder,
62-
path: `${cldAssetUrl}/:splat`,
63-
uploadPreset
64-
});
65+
const images = await Promise.all(imagesFiles.map(async image => {
66+
const publishPath = image.replace(PUBLISH_DIR, '');
67+
const publishUrl = `${host}${publishPath}`;
68+
const cldAssetPath = `/${path.join(PUBLIC_ASSET_PATH, publishPath)}`;
69+
const cldAssetUrl = `${host}${cldAssetPath}`;
6570

66-
netlifyConfig.redirects.unshift({
67-
from: `${cldAssetPath}*`,
68-
to: `${mediaPath}/:splat`,
69-
status: 200,
70-
force: true
71+
const cloudinary = await getCloudinaryUrl({
72+
deliveryType,
73+
folder,
74+
path: publishPath,
75+
localDir: PUBLISH_DIR,
76+
uploadPreset,
77+
remoteHost: host,
7178
});
7279

73-
netlifyConfig.redirects.unshift({
74-
from: `${mediaPath}/*`,
75-
to: assetRedirectUrl,
76-
status: 302,
77-
force: true
78-
});
80+
return {
81+
publishPath,
82+
publishUrl,
83+
...cloudinary
84+
}
7985
}));
86+
87+
netlifyConfig.build.environment.CLOUDINARY_ASSETS = {
88+
images
89+
}
90+
},
91+
92+
async onBuild({ netlifyConfig, constants, inputs }) {
93+
const host = process.env.DEPLOY_PRIME_URL || process.env.NETLIFY_HOST;
94+
95+
if ( !host ) {
96+
console.warn('Cannot determine Netlify host, not proceeding with on-page image replacement.');
97+
console.log('Note: The Netlify CLI does not currently support the ability to determine the host locally, try deploying on Netlify.');
98+
return;
99+
}
100+
101+
const { PUBLISH_DIR } = constants;
102+
103+
const {
104+
deliveryType,
105+
uploadPreset,
106+
folder = process.env.SITE_NAME
107+
} = inputs;
108+
109+
const cloudName = process.env.CLOUDINARY_CLOUD_NAME || inputs.cloudName;
110+
const apiKey = process.env.CLOUDINARY_API_KEY;
111+
const apiSecret = process.env.CLOUDINARY_API_SECRET;
112+
113+
if ( !cloudName ) {
114+
throw new Error('A Cloudinary Cloud Name is required. Please set cloudName input or use the environment variable CLOUDINARY_CLOUD_NAME');
115+
}
116+
117+
configureCloudinary({
118+
cloudName,
119+
apiKey,
120+
apiSecret
121+
});
122+
123+
// If the delivery type is set to upload, we need to be able to map individual assets based on their public ID,
124+
// which would require a dynamic middle solution, but that adds more hops than we want, so add a new redirect
125+
// for each asset uploaded
126+
127+
if ( deliveryType === 'upload' ) {
128+
await Promise.all(Object.keys(netlifyConfig.build.environment.CLOUDINARY_ASSETS).flatMap(mediaType => {
129+
return netlifyConfig.build.environment.CLOUDINARY_ASSETS[mediaType].map(async asset => {
130+
const { publishPath, cloudinaryUrl } = asset;
131+
132+
netlifyConfig.redirects.unshift({
133+
from: `${publishPath}*`,
134+
to: cloudinaryUrl,
135+
status: 302,
136+
force: true
137+
});
138+
})
139+
}));
140+
}
141+
142+
// If the delivery type is fetch, we're able to use the public URL and pass it right along "as is", so
143+
// we can create generic redirects. The tricky thing is to avoid a redirect loop, we modify the
144+
// path, so that we can safely allow Cloudinary to fetch the media remotely
145+
146+
if ( deliveryType === 'fetch' ) {
147+
await Promise.all(CLOUDINARY_ASSET_DIRECTORIES.map(async ({ name: mediaName, inputKey, path: defaultPath }) => {
148+
const mediaPath = inputs[inputKey] || defaultPath;
149+
const cldAssetPath = `/${path.join(PUBLIC_ASSET_PATH, mediaPath)}`;
150+
const cldAssetUrl = `${host}/${cldAssetPath}`;
151+
152+
const { cloudinaryUrl: assetRedirectUrl } = await getCloudinaryUrl({
153+
deliveryType: 'fetch',
154+
folder,
155+
path: `${cldAssetUrl}/:splat`,
156+
uploadPreset
157+
});
158+
159+
netlifyConfig.redirects.unshift({
160+
from: `${cldAssetPath}/*`,
161+
to: `${mediaPath}/:splat`,
162+
status: 200,
163+
force: true
164+
});
165+
166+
netlifyConfig.redirects.unshift({
167+
from: `${mediaPath}/*`,
168+
to: assetRedirectUrl,
169+
status: 302,
170+
force: true
171+
});
172+
}));
173+
}
174+
80175
},
81176

82177
// Post build looks through all of the output HTML and rewrites any src attributes to use a cloudinary URL
83178
// This only solves on-page references until any JS refreshes the DOM
84179

85-
async onPostBuild({ constants, inputs }) {
86-
87-
const host = process.env.DEPLOY_PRIME_URL;
180+
async onPostBuild({ netlifyConfig, constants, inputs }) {
181+
const host = process.env.DEPLOY_PRIME_URL || process.env.NETLIFY_HOST;
88182

89183
if ( !host ) {
90184
console.warn('Cannot determine Netlify host, not proceeding with on-page image replacement.');
@@ -121,6 +215,7 @@ module.exports = {
121215
const sourceHtml = await fs.readFile(page, 'utf-8');
122216

123217
const { html, errors } = await updateHtmlImagesToCloudinary(sourceHtml, {
218+
assets: netlifyConfig.build.environment.CLOUDINARY_ASSETS,
124219
deliveryType,
125220
uploadPreset,
126221
folder,

src/lib/cloudinary.js

Lines changed: 45 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -82,12 +82,14 @@ async function getCloudinaryUrl(options = {}) {
8282
}
8383

8484
let fileLocation;
85+
let publicId;
8586

8687
if ( deliveryType === 'fetch' ) {
8788
// fetch allows us to pass in a remote URL to the Cloudinary API
8889
// which it will cache and serve from the CDN, but not store
8990

9091
fileLocation = determineRemoteUrl(filePath, remoteHost);
92+
publicId = fileLocation;
9193
} else if ( deliveryType === 'upload' ) {
9294
// upload will actually store the image in the Cloudinary account
9395
// and subsequently serve that stored image
@@ -136,10 +138,11 @@ async function getCloudinaryUrl(options = {}) {
136138
// Finally use the stored public ID to grab the image URL
137139

138140
const { public_id } = results;
139-
fileLocation = public_id;
141+
publicId = public_id;
142+
fileLocation = fullPath;
140143
}
141144

142-
const cloudinaryUrl = cloudinary.url(fileLocation, {
145+
const cloudinaryUrl = cloudinary.url(publicId, {
143146
type: deliveryType,
144147
secure: true,
145148
transformation: [
@@ -150,7 +153,11 @@ async function getCloudinaryUrl(options = {}) {
150153
]
151154
});
152155

153-
return cloudinaryUrl;
156+
return {
157+
sourceUrl: fileLocation,
158+
cloudinaryUrl,
159+
publicId
160+
};
154161
}
155162

156163
module.exports.getCloudinaryUrl = getCloudinaryUrl;
@@ -161,6 +168,7 @@ module.exports.getCloudinaryUrl = getCloudinaryUrl;
161168

162169
async function updateHtmlImagesToCloudinary(html, options = {}) {
163170
const {
171+
assets,
164172
deliveryType,
165173
uploadPreset,
166174
folder,
@@ -178,27 +186,44 @@ async function updateHtmlImagesToCloudinary(html, options = {}) {
178186

179187
for ( const $img of images ) {
180188
let imgSrc = $img.getAttribute('src');
189+
let cloudinaryUrl;
181190

182-
try {
183-
const cloudinarySrc = await getCloudinaryUrl({
184-
deliveryType,
185-
folder,
186-
path: imgSrc,
187-
localDir,
188-
uploadPreset,
189-
remoteHost
190-
});
191+
// Check to see if we have an existing asset already to pick from
192+
// Look at both the path and full URL
191193

192-
$img.setAttribute('src', cloudinarySrc)
193-
} catch(e) {
194-
const { error } = e;
195-
errors.push({
196-
imgSrc,
197-
message: e.message || error.message
198-
});
199-
continue;
194+
const asset = Array.isArray(assets?.images) && assets.images.find(({ publishPath, publishUrl } = {}) => {
195+
return [publishPath, publishUrl].includes(imgSrc);
196+
});
197+
198+
if ( asset ) {
199+
cloudinaryUrl = asset.cloudinaryUrl;
200+
}
201+
202+
// If we don't have an asset and thus don't have a Cloudinary URL, create
203+
// one for our asset
204+
205+
if ( !cloudinaryUrl ) {
206+
try {
207+
const { cloudinaryUrl: url } = await getCloudinaryUrl({
208+
deliveryType,
209+
folder,
210+
path: imgSrc,
211+
localDir,
212+
uploadPreset,
213+
remoteHost
214+
});
215+
cloudinaryUrl = url;
216+
} catch(e) {
217+
const { error } = e;
218+
errors.push({
219+
imgSrc,
220+
message: e.message || error.message
221+
});
222+
continue;
223+
}
200224
}
201225

226+
$img.setAttribute('src', cloudinaryUrl)
202227
}
203228

204229
return {

tests/lib/cloudinary.test.js

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -21,65 +21,65 @@ describe('lib/util', () => {
2121
});
2222

2323
describe('createPublicId', () => {
24-
24+
2525
test('should create a public ID from a remote URL', async () => {
2626
const mikeId = await createPublicId({ path: 'https://i.imgur.com/e6XK75j.png' });
2727
expect(mikeId).toEqual('e6XK75j-58e290136642a9c711afa6410b07848d');
2828

2929
const lucasId = await createPublicId({ path: 'https://i.imgur.com/vtYmp1x.png' });
3030
expect(lucasId).toEqual('vtYmp1x-ae71a79c9c36b8d5dba872c3b274a444');
3131
});
32-
32+
3333
test('should create a public ID from a local image', async () => {
3434
const dustinId = await createPublicId({ path: '../images/stranger-things-dustin.jpeg' });
3535
expect(dustinId).toEqual('stranger-things-dustin-9a2a7b1501695c50ad85c329f79fb184');
36-
36+
3737
const elevenId = await createPublicId({ path: '../images/stranger-things-eleven.jpeg' });
3838
expect(elevenId).toEqual('stranger-things-eleven-c5486e412115dbeba03315959c3a6d20');
3939
});
4040

4141
});
4242

4343
describe('getCloudinaryUrl', () => {
44-
44+
4545
test('should create a Cloudinary URL with delivery type of fetch from a local image', async () => {
46-
const url = await getCloudinaryUrl({
46+
const { cloudinaryUrl } = await getCloudinaryUrl({
4747
deliveryType: 'fetch',
4848
path: '/images/stranger-things-dustin.jpeg',
4949
localDir: '/tests/images',
5050
remoteHost: 'https://cloudinary.netlify.app'
5151
});
5252

53-
expect(url).toEqual(`https://res.cloudinary.com/${process.env.CLOUDINARY_CLOUD_NAME}/image/fetch/f_auto,q_auto/https://cloudinary.netlify.app/images/stranger-things-dustin.jpeg`);
53+
expect(cloudinaryUrl).toEqual(`https://res.cloudinary.com/${process.env.CLOUDINARY_CLOUD_NAME}/image/fetch/f_auto,q_auto/https://cloudinary.netlify.app/images/stranger-things-dustin.jpeg`);
5454
});
5555

5656
test('should create a Cloudinary URL with delivery type of fetch from a remote image', async () => {
57-
const url = await getCloudinaryUrl({
57+
const { cloudinaryUrl } = await getCloudinaryUrl({
5858
deliveryType: 'fetch',
5959
path: 'https://i.imgur.com/vtYmp1x.png'
6060
});
6161

62-
expect(url).toEqual(`https://res.cloudinary.com/${process.env.CLOUDINARY_CLOUD_NAME}/image/fetch/f_auto,q_auto/https://i.imgur.com/vtYmp1x.png`);
62+
expect(cloudinaryUrl).toEqual(`https://res.cloudinary.com/${process.env.CLOUDINARY_CLOUD_NAME}/image/fetch/f_auto,q_auto/https://i.imgur.com/vtYmp1x.png`);
6363
});
64-
64+
6565
test('should create a Cloudinary URL with delivery type of upload from a local image', async () => {
66-
const url = await getCloudinaryUrl({
66+
const { cloudinaryUrl } = await getCloudinaryUrl({
6767
deliveryType: 'upload',
6868
path: '/images/stranger-things-dustin.jpeg',
6969
localDir: 'tests',
7070
remoteHost: 'https://cloudinary.netlify.app'
7171
});
7272

73-
expect(url).toEqual(`https://res.cloudinary.com/${process.env.CLOUDINARY_CLOUD_NAME}/image/upload/f_auto,q_auto/stranger-things-dustin-fc571e771d5ca7d9223a7eebfd2c505d`);
73+
expect(cloudinaryUrl).toEqual(`https://res.cloudinary.com/${process.env.CLOUDINARY_CLOUD_NAME}/image/upload/f_auto,q_auto/stranger-things-dustin-fc571e771d5ca7d9223a7eebfd2c505d`);
7474
});
75-
75+
7676
test('should create a Cloudinary URL with delivery type of upload from a remote image', async () => {
77-
const url = await getCloudinaryUrl({
77+
const { cloudinaryUrl } = await getCloudinaryUrl({
7878
deliveryType: 'upload',
7979
path: 'https://i.imgur.com/vtYmp1x.png'
8080
});
8181

82-
expect(url).toEqual(`https://res.cloudinary.com/${process.env.CLOUDINARY_CLOUD_NAME}/image/upload/f_auto,q_auto/vtYmp1x-ae71a79c9c36b8d5dba872c3b274a444`);
82+
expect(cloudinaryUrl).toEqual(`https://res.cloudinary.com/${process.env.CLOUDINARY_CLOUD_NAME}/image/upload/f_auto,q_auto/vtYmp1x-ae71a79c9c36b8d5dba872c3b274a444`);
8383
});
8484

8585
});

0 commit comments

Comments
 (0)