Skip to content

Commit 2984719

Browse files
authored
Supported __GHOST_URL__ transforms for CDN asset URLs
ref https://linear.app/ghost/issue/PRO-1524 In order to correctly store assets URLs when they are on a CDN we need to support transforming more than just the siteUrl to `__GHOST_URL__`. This implementation allows for custom asset urls for images, media & files and supports these urls including a subdomain. The bulk of the logic is in the `absoluteToTransformReady` & `transformReadyToAbsolute` utils which are then used by all of the more specific transforms. The html & markdown transforms have an early return that we needed to modify to ensure still runs for CDN urls The plaintext transform had a regex to optimise the replacements which needed updating to match the CDN urls too. We also export the static constants for consumption by Ghost, in-line with the STATIC_IMAGE_PREFIX pattern we have.
1 parent 18d7d9f commit 2984719

16 files changed

Lines changed: 1053 additions & 139 deletions

packages/url-utils/lib/UrlUtils.js

Lines changed: 98 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -23,23 +23,53 @@ module.exports = class UrlUtils {
2323
* @param {Object} [options.slugs] object with 2 properties reserved and protected containing arrays of special case slugs
2424
* @param {Number} [options.redirectCacheMaxAge]
2525
* @param {String} [options.staticImageUrlPrefix='content/images'] static prefix for serving images. Should not be passed in, unless customizing ghost instance image storage
26+
* @param {String} [options.staticFilesUrlPrefix='content/files'] static prefix for serving files. Should not be passed in, unless customizing ghost instance file storage
27+
* @param {String} [options.staticMediaUrlPrefix='content/media'] static prefix for serving media. Should not be passed in, unless customizing ghost instance media storage
28+
* @param {object} [options.assetBaseUrls] asset CDN base URLs
29+
* @param {string} [options.assetBaseUrls.image] image asset CDN base URL
30+
* @param {string} [options.assetBaseUrls.files] files asset CDN base URL
31+
* @param {string} [options.assetBaseUrls.media] media asset CDN base URL
2632
*/
2733
constructor(options = {}) {
2834
const defaultOptions = {
2935
slugs: null,
3036
redirectCacheMaxAge: null,
3137
baseApiPath: '/ghost/api',
3238
defaultApiType: 'content',
33-
staticImageUrlPrefix: 'content/images'
39+
staticImageUrlPrefix: 'content/images',
40+
staticFilesUrlPrefix: 'content/files',
41+
staticMediaUrlPrefix: 'content/media'
3442
};
3543

3644
this._config = assignOptions({}, defaultOptions, options);
3745

46+
const assetBaseUrls = options.assetBaseUrls || {};
47+
this._assetBaseUrls = {
48+
image: assetBaseUrls.image || null,
49+
files: assetBaseUrls.files || null,
50+
media: assetBaseUrls.media || null
51+
};
52+
3853
this.getSubdir = options.getSubdir;
3954
this.getSiteUrl = options.getSiteUrl;
4055
this.getAdminUrl = options.getAdminUrl;
4156
}
4257

58+
_assetOptionDefaults() {
59+
return {
60+
staticImageUrlPrefix: this._config.staticImageUrlPrefix,
61+
staticFilesUrlPrefix: this._config.staticFilesUrlPrefix,
62+
staticMediaUrlPrefix: this._config.staticMediaUrlPrefix,
63+
imageBaseUrl: this._assetBaseUrls.image || null,
64+
filesBaseUrl: this._assetBaseUrls.files || null,
65+
mediaBaseUrl: this._assetBaseUrls.media || null
66+
};
67+
}
68+
69+
_buildAssetOptions(additionalDefaults = {}, options) {
70+
return assignOptions({}, this._assetOptionDefaults(), additionalDefaults, options || {});
71+
}
72+
4373
getProtectedSlugs() {
4474
let subDir = this.getSubdir();
4575

@@ -234,31 +264,37 @@ module.exports = class UrlUtils {
234264
options = itemPath;
235265
itemPath = null;
236266
}
237-
return utils.toTransformReady(url, this.getSiteUrl(), itemPath, options);
267+
const _options = this._buildAssetOptions({}, options);
268+
return utils.toTransformReady(url, this.getSiteUrl(), itemPath, _options);
238269
}
239270

240271
absoluteToTransformReady(url, options) {
241-
return utils.absoluteToTransformReady(url, this.getSiteUrl(), options);
272+
const _options = this._buildAssetOptions({}, options);
273+
return utils.absoluteToTransformReady(url, this.getSiteUrl(), _options);
242274
}
243275

244276
relativeToTransformReady(url, options) {
245-
return utils.relativeToTransformReady(url, this.getSiteUrl(), options);
277+
const _options = this._buildAssetOptions({}, options);
278+
return utils.relativeToTransformReady(url, this.getSiteUrl(), _options);
246279
}
247280

248281
transformReadyToAbsolute(url, options) {
249-
return utils.transformReadyToAbsolute(url, this.getSiteUrl(), options);
282+
const _options = this._buildAssetOptions({}, options);
283+
return utils.transformReadyToAbsolute(url, this.getSiteUrl(), _options);
250284
}
251285

252286
transformReadyToRelative(url, options) {
253-
return utils.transformReadyToRelative(url, this.getSiteUrl(), options);
287+
const _options = this._buildAssetOptions({}, options);
288+
return utils.transformReadyToRelative(url, this.getSiteUrl(), _options);
254289
}
255290

256291
htmlToTransformReady(html, itemPath, options) {
257292
if (typeof itemPath === 'object' && !options) {
258293
options = itemPath;
259294
itemPath = null;
260295
}
261-
return utils.htmlToTransformReady(html, this.getSiteUrl(), itemPath, options);
296+
const _options = this._buildAssetOptions({}, options);
297+
return utils.htmlToTransformReady(html, this.getSiteUrl(), itemPath, _options);
262298
}
263299

264300
/**
@@ -276,11 +312,9 @@ module.exports = class UrlUtils {
276312
options = itemPath;
277313
itemPath = null;
278314
}
279-
const defaultOptions = {
280-
assetsOnly: false,
281-
staticImageUrlPrefix: this._config.staticImageUrlPrefix
282-
};
283-
const _options = assignOptions({}, defaultOptions, options || {});
315+
const _options = this._buildAssetOptions({
316+
assetsOnly: false
317+
}, options);
284318
return utils.htmlRelativeToAbsolute(html, this.getSiteUrl(), itemPath, _options);
285319
}
286320

@@ -289,29 +323,23 @@ module.exports = class UrlUtils {
289323
options = itemPath;
290324
itemPath = null;
291325
}
292-
const defaultOptions = {
293-
assetsOnly: false,
294-
staticImageUrlPrefix: this._config.staticImageUrlPrefix
295-
};
296-
const _options = assignOptions({}, defaultOptions, options || {});
326+
const _options = this._buildAssetOptions({
327+
assetsOnly: false
328+
}, options);
297329
return utils.htmlRelativeToTransformReady(html, this.getSiteUrl(), itemPath, _options);
298330
}
299331

300332
htmlAbsoluteToRelative(html, options = {}) {
301-
const defaultOptions = {
302-
assetsOnly: false,
303-
staticImageUrlPrefix: this._config.staticImageUrlPrefix
304-
};
305-
const _options = assignOptions({}, defaultOptions, options);
333+
const _options = this._buildAssetOptions({
334+
assetsOnly: false
335+
}, options);
306336
return utils.htmlAbsoluteToRelative(html, this.getSiteUrl(), _options);
307337
}
308338

309339
htmlAbsoluteToTransformReady(html, options = {}) {
310-
const defaultOptions = {
311-
assetsOnly: false,
312-
staticImageUrlPrefix: this._config.staticImageUrlPrefix
313-
};
314-
const _options = assignOptions({}, defaultOptions, options);
340+
const _options = this._buildAssetOptions({
341+
assetsOnly: false
342+
}, options);
315343
return utils.htmlAbsoluteToTransformReady(html, this.getSiteUrl(), _options);
316344
}
317345

@@ -320,19 +348,18 @@ module.exports = class UrlUtils {
320348
options = itemPath;
321349
itemPath = null;
322350
}
323-
return utils.markdownToTransformReady(markdown, this.getSiteUrl(), itemPath, options);
351+
const _options = this._buildAssetOptions({}, options);
352+
return utils.markdownToTransformReady(markdown, this.getSiteUrl(), itemPath, _options);
324353
}
325354

326355
markdownRelativeToAbsolute(markdown, itemPath, options) {
327356
if (typeof itemPath === 'object' && !options) {
328357
options = itemPath;
329358
itemPath = null;
330359
}
331-
const defaultOptions = {
332-
assetsOnly: false,
333-
staticImageUrlPrefix: this._config.staticImageUrlPrefix
334-
};
335-
const _options = assignOptions({}, defaultOptions, options || {});
360+
const _options = this._buildAssetOptions({
361+
assetsOnly: false
362+
}, options);
336363
return utils.markdownRelativeToAbsolute(markdown, this.getSiteUrl(), itemPath, _options);
337364
}
338365

@@ -341,29 +368,23 @@ module.exports = class UrlUtils {
341368
options = itemPath;
342369
itemPath = null;
343370
}
344-
const defaultOptions = {
345-
assetsOnly: false,
346-
staticImageUrlPrefix: this._config.staticImageUrlPrefix
347-
};
348-
const _options = assignOptions({}, defaultOptions, options || {});
371+
const _options = this._buildAssetOptions({
372+
assetsOnly: false
373+
}, options);
349374
return utils.markdownRelativeToTransformReady(markdown, this.getSiteUrl(), itemPath, _options);
350375
}
351376

352377
markdownAbsoluteToRelative(markdown, options = {}) {
353-
const defaultOptions = {
354-
assetsOnly: false,
355-
staticImageUrlPrefix: this._config.staticImageUrlPrefix
356-
};
357-
const _options = assignOptions({}, defaultOptions, options);
378+
const _options = this._buildAssetOptions({
379+
assetsOnly: false
380+
}, options);
358381
return utils.markdownAbsoluteToRelative(markdown, this.getSiteUrl(), _options);
359382
}
360383

361384
markdownAbsoluteToTransformReady(markdown, options) {
362-
const defaultOptions = {
363-
assetsOnly: false,
364-
staticImageUrlPrefix: this._config.staticImageUrlPrefix
365-
};
366-
const _options = assignOptions({}, defaultOptions, options);
385+
const _options = this._buildAssetOptions({
386+
assetsOnly: false
387+
}, options);
367388
return utils.markdownAbsoluteToTransformReady(markdown, this.getSiteUrl(), _options);
368389
}
369390

@@ -372,10 +393,9 @@ module.exports = class UrlUtils {
372393
options = itemPath;
373394
itemPath = null;
374395
}
375-
const defaultOptions = {
396+
const _options = this._buildAssetOptions({
376397
cardTransformers: this._config.cardTransformers
377-
};
378-
const _options = assignOptions({}, defaultOptions, options || {});
398+
}, options);
379399
return utils.mobiledocToTransformReady(serializedMobiledoc, this.getSiteUrl(), itemPath, _options);
380400
}
381401

@@ -384,12 +404,10 @@ module.exports = class UrlUtils {
384404
options = itemPath;
385405
itemPath = null;
386406
}
387-
const defaultOptions = {
407+
const _options = this._buildAssetOptions({
388408
assetsOnly: false,
389-
staticImageUrlPrefix: this._config.staticImageUrlPrefix,
390409
cardTransformers: this._config.cardTransformers
391-
};
392-
const _options = assignOptions({}, defaultOptions, options || {});
410+
}, options);
393411
return utils.mobiledocRelativeToAbsolute(serializedMobiledoc, this.getSiteUrl(), itemPath, _options);
394412
}
395413

@@ -398,32 +416,26 @@ module.exports = class UrlUtils {
398416
options = itemPath;
399417
itemPath = null;
400418
}
401-
const defaultOptions = {
419+
const _options = this._buildAssetOptions({
402420
assetsOnly: false,
403-
staticImageUrlPrefix: this._config.staticImageUrlPrefix,
404421
cardTransformers: this._config.cardTransformers
405-
};
406-
const _options = assignOptions({}, defaultOptions, options || {});
422+
}, options);
407423
return utils.mobiledocRelativeToTransformReady(serializedMobiledoc, this.getSiteUrl(), itemPath, _options);
408424
}
409425

410426
mobiledocAbsoluteToRelative(serializedMobiledoc, options = {}) {
411-
const defaultOptions = {
427+
const _options = this._buildAssetOptions({
412428
assetsOnly: false,
413-
staticImageUrlPrefix: this._config.staticImageUrlPrefix,
414429
cardTransformers: this._config.cardTransformers
415-
};
416-
const _options = assignOptions({}, defaultOptions, options);
430+
}, options);
417431
return utils.mobiledocAbsoluteToRelative(serializedMobiledoc, this.getSiteUrl(), _options);
418432
}
419433

420434
mobiledocAbsoluteToTransformReady(serializedMobiledoc, options = {}) {
421-
const defaultOptions = {
435+
const _options = this._buildAssetOptions({
422436
assetsOnly: false,
423-
staticImageUrlPrefix: this._config.staticImageUrlPrefix,
424437
cardTransformers: this._config.cardTransformers
425-
};
426-
const _options = assignOptions({}, defaultOptions, options);
438+
}, options);
427439
return utils.mobiledocAbsoluteToTransformReady(serializedMobiledoc, this.getSiteUrl(), _options);
428440
}
429441

@@ -432,10 +444,9 @@ module.exports = class UrlUtils {
432444
options = itemPath;
433445
itemPath = null;
434446
}
435-
const defaultOptions = {
447+
const _options = this._buildAssetOptions({
436448
cardTransformers: this._config.cardTransformers
437-
};
438-
const _options = assignOptions({}, defaultOptions, options || {});
449+
}, options);
439450
return utils.lexicalToTransformReady(serializedLexical, this.getSiteUrl(), itemPath, _options);
440451
}
441452

@@ -444,12 +455,10 @@ module.exports = class UrlUtils {
444455
options = itemPath;
445456
itemPath = null;
446457
}
447-
const defaultOptions = {
458+
const _options = this._buildAssetOptions({
448459
assetsOnly: false,
449-
staticImageUrlPrefix: this._config.staticImageUrlPrefix,
450460
cardTransformers: this._config.cardTransformers
451-
};
452-
const _options = assignOptions({}, defaultOptions, options || {});
461+
}, options);
453462
return utils.lexicalRelativeToAbsolute(serializedLexical, this.getSiteUrl(), itemPath, _options);
454463
}
455464

@@ -458,40 +467,31 @@ module.exports = class UrlUtils {
458467
options = itemPath;
459468
itemPath = null;
460469
}
461-
const defaultOptions = {
470+
const _options = this._buildAssetOptions({
462471
assetsOnly: false,
463-
staticImageUrlPrefix: this._config.staticImageUrlPrefix,
464472
cardTransformers: this._config.cardTransformers
465-
};
466-
const _options = assignOptions({}, defaultOptions, options || {});
473+
}, options);
467474
return utils.lexicalRelativeToTransformReady(serializedLexical, this.getSiteUrl(), itemPath, _options);
468475
}
469476

470477
lexicalAbsoluteToRelative(serializedLexical, options = {}) {
471-
const defaultOptions = {
478+
const _options = this._buildAssetOptions({
472479
assetsOnly: false,
473-
staticImageUrlPrefix: this._config.staticImageUrlPrefix,
474480
cardTransformers: this._config.cardTransformers
475-
};
476-
const _options = assignOptions({}, defaultOptions, options);
481+
}, options);
477482
return utils.lexicalAbsoluteToRelative(serializedLexical, this.getSiteUrl(), _options);
478483
}
479484

480485
lexicalAbsoluteToTransformReady(serializedLexical, options = {}) {
481-
const defaultOptions = {
486+
const _options = this._buildAssetOptions({
482487
assetsOnly: false,
483-
staticImageUrlPrefix: this._config.staticImageUrlPrefix,
484488
cardTransformers: this._config.cardTransformers
485-
};
486-
const _options = assignOptions({}, defaultOptions, options);
489+
}, options);
487490
return utils.lexicalAbsoluteToTransformReady(serializedLexical, this.getSiteUrl(), _options);
488491
}
489492

490493
plaintextToTransformReady(plaintext, options = {}) {
491-
const defaultOptions = {
492-
staticImageUrlPrefix: this._config.staticImageUrlPrefix
493-
};
494-
const _options = assignOptions({}, defaultOptions, options);
494+
const _options = this._buildAssetOptions({}, options);
495495
return utils.plaintextToTransformReady(plaintext, this.getSiteUrl(), _options);
496496
}
497497

@@ -537,6 +537,14 @@ module.exports = class UrlUtils {
537537
return this._config.staticImageUrlPrefix;
538538
}
539539

540+
get STATIC_FILES_URL_PREFIX() {
541+
return this._config.staticFilesUrlPrefix;
542+
}
543+
544+
get STATIC_MEDIA_URL_PREFIX() {
545+
return this._config.staticMediaUrlPrefix;
546+
}
547+
540548
// expose underlying functions to ease testing
541549
get _utils() {
542550
return utils;

0 commit comments

Comments
 (0)