Skip to content

Commit f429986

Browse files
authored
Merge pull request #12 from colbyfayock/individual-redirects
Redirect media directly to a Cloudinary URL
2 parents c79f8c5 + dbd6186 commit f429986

File tree

10 files changed

+227
-167
lines changed

10 files changed

+227
-167
lines changed

.github/workflows/tests.yml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,21 @@ on: [push, pull_request]
55
jobs:
66
tests:
77
runs-on: ubuntu-latest
8-
8+
99
strategy:
1010
matrix:
1111
node: [ '12', '14', '16' ]
12-
12+
1313
steps:
1414
- uses: actions/checkout@v2
1515
- uses: actions/setup-node@v2
1616
with:
1717
node-version: ${{ matrix.node }}
18-
18+
1919
- run: yarn install --frozen-lockfile
2020
- run: yarn test
2121
env:
2222
CLOUDINARY_CLOUD_NAME: ${{ secrets.CLOUDINARY_CLOUD_NAME }}
2323
CLOUDINARY_API_KEY: ${{ secrets.CLOUDINARY_API_KEY }}
2424
CLOUDINARY_API_SECRET: ${{ secrets.CLOUDINARY_API_SECRET }}
25+
NETLIFY_HOST: ${{ secrets.NETLIFY_HOST }} # Used to test functionality outside of the Netlify environment

demo/public/images/test/beach.jpeg

553 KB
Loading

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"

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
},
1414
"license": "MIT",
1515
"dependencies": {
16-
"@vercel/ncc": "^0.33.1",
1716
"cloudinary": "^1.27.1",
1817
"fs-extra": "^10.0.0",
1918
"glob": "^7.2.0",

src/index.js

Lines changed: 139 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
const fs = require('fs-extra')
22
const path = require('path');
33
const glob = require('glob');
4-
const ncc = require('@vercel/ncc');
54

6-
const { configureCloudinary, updateHtmlImagesToCloudinary } = require('./lib/cloudinary');
7-
const { PREFIX, PUBLIC_ASSET_PATH } = require('./data/cloudinary');
5+
const { configureCloudinary, updateHtmlImagesToCloudinary, getCloudinaryUrl } = require('./lib/cloudinary');
6+
const { PUBLIC_ASSET_PATH } = require('./data/cloudinary');
87

9-
const CLOUDINARY_MEDIA_FUNCTIONS = [
8+
const CLOUDINARY_ASSET_DIRECTORIES = [
109
{
1110
name: 'images',
1211
inputKey: 'imagesPath',
@@ -20,109 +19,186 @@ const CLOUDINARY_MEDIA_FUNCTIONS = [
2019
*/
2120

2221
module.exports = {
22+
async onPreBuild({ netlifyConfig, constants, inputs }) {
23+
const host = process.env.DEPLOY_PRIME_URL || process.env.NETLIFY_HOST;
24+
25+
if ( !host ) {
26+
console.warn('Cannot determine Netlify host, not proceeding with on-page image replacement.');
27+
console.log('Note: The Netlify CLI does not currently support the ability to determine the host locally, try deploying on Netlify.');
28+
return;
29+
}
30+
31+
const { PUBLISH_DIR } = constants;
2332

24-
async onBuild({ netlifyConfig, constants, inputs }) {
25-
const { FUNCTIONS_SRC, INTERNAL_FUNCTIONS_SRC } = constants;
2633
const {
2734
deliveryType,
28-
folder = process.env.SITE_NAME,
2935
uploadPreset,
36+
folder = process.env.SITE_NAME
3037
} = inputs;
3138

39+
// If we're using the fetch API, we don't need to worry about uploading any
40+
// of the media as it will 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+
3247
const cloudName = process.env.CLOUDINARY_CLOUD_NAME || inputs.cloudName;
3348

49+
const apiKey = process.env.CLOUDINARY_API_KEY;
50+
const apiSecret = process.env.CLOUDINARY_API_SECRET;
51+
3452
if ( !cloudName ) {
3553
throw new Error('A Cloudinary Cloud Name is required. Please set cloudName input or use the environment variable CLOUDINARY_CLOUD_NAME');
3654
}
3755

38-
const functionsPath = INTERNAL_FUNCTIONS_SRC || FUNCTIONS_SRC;
56+
configureCloudinary({
57+
cloudName,
58+
apiKey,
59+
apiSecret
60+
});
3961

40-
// Copy all of the templates over including the functions to deploy
62+
const imagesDirectory = glob.sync(`${PUBLISH_DIR}/images/**/*`);
63+
const imagesFiles = imagesDirectory.filter(file => !!path.extname(file));
4164

42-
const functionTemplatesPath = path.join(__dirname, 'templates/functions');
43-
const functionTemplates = await fs.readdir(functionTemplatesPath);
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}`;
4470

45-
if ( !Array.isArray(functionTemplates) || functionTemplates.length == 0 ) {
46-
throw new Error(`Failed to generate templates: can not find function templates in ${functionTemplatesPath}`);
47-
}
71+
const cloudinary = await getCloudinaryUrl({
72+
deliveryType,
73+
folder,
74+
path: publishPath,
75+
localDir: PUBLISH_DIR,
76+
uploadPreset,
77+
remoteHost: host,
78+
});
4879

49-
try {
50-
await Promise.all(functionTemplates.map(async templateFileName => {
51-
const bundle = await ncc(path.join(functionTemplatesPath, templateFileName));
52-
const { name, base } = path.parse(templateFileName);
53-
const templateDirectory = path.join(functionsPath, name);
54-
const filePath = path.join(templateDirectory, base);
80+
return {
81+
publishPath,
82+
publishUrl,
83+
...cloudinary
84+
}
85+
}));
5586

56-
await fs.ensureDir(templateDirectory);
57-
await fs.writeFile(filePath, bundle.code, 'utf8');
58-
}));
59-
} catch(e) {
60-
throw new Error(`Failed to generate templates: ${e}`);
87+
netlifyConfig.build.environment.CLOUDINARY_ASSETS = {
88+
images
6189
}
90+
},
6291

63-
// Configure reference parameters for Cloudinary delivery to attach to redirect
92+
async onBuild({ netlifyConfig, constants, inputs }) {
93+
const host = process.env.DEPLOY_PRIME_URL || process.env.NETLIFY_HOST;
6494

65-
const params = {
66-
uploadPreset,
67-
deliveryType,
68-
cloudName,
69-
folder
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;
7099
}
71100

72-
const paramsString = Object.keys(params)
73-
.filter(key => typeof params[key] !== 'undefined')
74-
.map(key => `${key}=${encodeURIComponent(params[key])}`)
75-
.join('&');
101+
const { PUBLISH_DIR } = constants;
76102

77-
// Redirect any requests that hits /[media type]/* to a serverless function
103+
const {
104+
deliveryType,
105+
uploadPreset,
106+
folder = process.env.SITE_NAME
107+
} = inputs;
78108

79-
CLOUDINARY_MEDIA_FUNCTIONS.forEach(({ name: mediaName, inputKey, path: defaultPath }) => {
80-
const mediaPath = inputs[inputKey] || defaultPath;
81-
const mediaPathSplat = path.join(mediaPath, ':splat');
82-
const functionName = `${PREFIX}_${mediaName}`;
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;
83112

84-
netlifyConfig.redirects.unshift({
85-
from: path.join(PUBLIC_ASSET_PATH, mediaPath, '*'),
86-
to: mediaPathSplat,
87-
status: 200,
88-
force: true
89-
});
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+
}
90116

91-
netlifyConfig.redirects.unshift({
92-
from: path.join(mediaPath, '*'),
93-
to: `/.netlify/functions/${functionName}?path=${mediaPathSplat}&${paramsString}`,
94-
status: 302,
95-
force: true,
96-
});
117+
configureCloudinary({
118+
cloudName,
119+
apiKey,
120+
apiSecret
97121
});
98122

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+
99175
},
100176

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

104-
async onPostBuild({ constants, inputs }) {
105-
const { PUBLISH_DIR } = constants;
106-
const {
107-
deliveryType,
108-
uploadPreset,
109-
folder = process.env.SITE_NAME
110-
} = inputs;
111-
112-
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;
113182

114183
if ( !host ) {
115184
console.warn('Cannot determine Netlify host, not proceeding with on-page image replacement.');
116185
console.log('Note: The Netlify CLI does not currently support the ability to determine the host locally, try deploying on Netlify.');
117186
return;
118187
}
119188

189+
const { PUBLISH_DIR } = constants;
190+
const {
191+
deliveryType,
192+
uploadPreset,
193+
folder = process.env.SITE_NAME
194+
} = inputs;
195+
120196
const cloudName = process.env.CLOUDINARY_CLOUD_NAME || inputs.cloudName;
121197
const apiKey = process.env.CLOUDINARY_API_KEY;
122198
const apiSecret = process.env.CLOUDINARY_API_SECRET;
123199

124200
if ( !cloudName ) {
125-
throw new Error('Cloudinary Cloud Name required. Please use an environment variable CLOUDINARY_CLOUD_NAME');
201+
throw new Error('A Cloudinary Cloud Name is required. Please set cloudName input or use the environment variable CLOUDINARY_CLOUD_NAME');
126202
}
127203

128204
configureCloudinary({
@@ -139,6 +215,7 @@ module.exports = {
139215
const sourceHtml = await fs.readFile(page, 'utf-8');
140216

141217
const { html, errors } = await updateHtmlImagesToCloudinary(sourceHtml, {
218+
assets: netlifyConfig.build.environment.CLOUDINARY_ASSETS,
142219
deliveryType,
143220
uploadPreset,
144221
folder,

0 commit comments

Comments
 (0)