mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-01-27 22:49:56 -05:00
Reduced size of social metadata images (#19048)
refs https://github.com/TryGhost/Product/issues/4140 - added `social-image` image size to our `internalImagesSizes` list with a max-width of 1200 - extracted image utils from `{{img_url}}` helper to a utils file for re-use - updated `getImageDimensions` method that reads image dimensions and modifies the finalised `metaData` object before use to adjust dimensions and associated URLs to match max width of 1200px
This commit is contained in:
parent
0cf25d0afe
commit
1be490ae9e
5 changed files with 207 additions and 104 deletions
|
@ -7,19 +7,21 @@
|
|||
// Returns the URL for the current object scope i.e. If inside a post scope will return image permalink
|
||||
// `absolute` flag outputs absolute URL, else URL is relative.
|
||||
const {urlUtils} = require('../services/proxy');
|
||||
const {
|
||||
detectInternalImage,
|
||||
getImageWithSize,
|
||||
getUnsplashImage,
|
||||
detectUnsplashImage
|
||||
} = require('../utils/images');
|
||||
|
||||
const url = require('url');
|
||||
const _ = require('lodash');
|
||||
const logging = require('@tryghost/logging');
|
||||
const tpl = require('@tryghost/tpl');
|
||||
const imageTransform = require('@tryghost/image-transform');
|
||||
|
||||
const messages = {
|
||||
attrIsRequired: 'Attribute is required e.g. {{img_url feature_image}}'
|
||||
};
|
||||
|
||||
const STATIC_IMAGE_URL_PREFIX = `${urlUtils.STATIC_IMAGE_URL_PREFIX}`;
|
||||
|
||||
module.exports = function imgUrl(requestedImageUrl, options) {
|
||||
// CASE: if no url is passed, e.g. `{{img_url}}` we show a warning
|
||||
if (arguments.length < 2) {
|
||||
|
@ -46,7 +48,7 @@ module.exports = function imgUrl(requestedImageUrl, options) {
|
|||
|
||||
if (!isInternalImage) {
|
||||
// Detect Unsplash width and format
|
||||
const isUnsplashImage = /images\.unsplash\.com/.test(requestedImageUrl);
|
||||
const isUnsplashImage = detectUnsplashImage(requestedImageUrl);
|
||||
if (isUnsplashImage) {
|
||||
try {
|
||||
return getUnsplashImage(requestedImageUrl, sizeOptions);
|
||||
|
@ -99,95 +101,3 @@ function getImageSizeOptions(options) {
|
|||
requestedFormat
|
||||
};
|
||||
}
|
||||
|
||||
function detectInternalImage(requestedImageUrl) {
|
||||
const siteUrl = urlUtils.getSiteUrl();
|
||||
const isAbsoluteImage = /https?:\/\//.test(requestedImageUrl);
|
||||
const isAbsoluteInternalImage = isAbsoluteImage && requestedImageUrl.startsWith(siteUrl);
|
||||
|
||||
// CASE: imagePath is a "protocol relative" url e.g. "//www.gravatar.com/ava..."
|
||||
// by resolving the the imagePath relative to the blog url, we can then
|
||||
// detect if the imagePath is external, or internal.
|
||||
const isRelativeInternalImage = !isAbsoluteImage && url.resolve(siteUrl, requestedImageUrl).startsWith(siteUrl);
|
||||
|
||||
return isAbsoluteInternalImage || isRelativeInternalImage;
|
||||
}
|
||||
|
||||
function getUnsplashImage(imagePath, sizeOptions) {
|
||||
const parsedUrl = new URL(imagePath);
|
||||
const {requestedSize, imageSizes, requestedFormat} = sizeOptions;
|
||||
|
||||
if (requestedFormat) {
|
||||
const supportedFormats = ['avif', 'gif', 'jpg', 'png', 'webp'];
|
||||
if (supportedFormats.includes(requestedFormat)) {
|
||||
parsedUrl.searchParams.set('fm', requestedFormat);
|
||||
} else if (requestedFormat === 'jpeg') {
|
||||
// Map to alias
|
||||
parsedUrl.searchParams.set('fm', 'jpg');
|
||||
}
|
||||
}
|
||||
|
||||
if (!imageSizes || !imageSizes[requestedSize]) {
|
||||
return parsedUrl.toString();
|
||||
}
|
||||
|
||||
const {width, height} = imageSizes[requestedSize];
|
||||
|
||||
if (!width && !height) {
|
||||
return parsedUrl.toString();
|
||||
}
|
||||
|
||||
parsedUrl.searchParams.delete('w');
|
||||
parsedUrl.searchParams.delete('h');
|
||||
|
||||
if (width) {
|
||||
parsedUrl.searchParams.set('w', width);
|
||||
}
|
||||
if (height) {
|
||||
parsedUrl.searchParams.set('h', height);
|
||||
}
|
||||
return parsedUrl.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} imagePath
|
||||
* @param {Object} sizeOptions
|
||||
* @param {string} sizeOptions.requestedSize
|
||||
* @param {Object[]} sizeOptions.imageSizes
|
||||
* @param {string} [sizeOptions.requestedFormat]
|
||||
* @returns
|
||||
*/
|
||||
function getImageWithSize(imagePath, sizeOptions) {
|
||||
const hasLeadingSlash = imagePath[0] === '/';
|
||||
|
||||
if (hasLeadingSlash) {
|
||||
return '/' + getImageWithSize(imagePath.slice(1), sizeOptions);
|
||||
}
|
||||
const {requestedSize, imageSizes, requestedFormat} = sizeOptions;
|
||||
|
||||
if (!requestedSize) {
|
||||
return imagePath;
|
||||
}
|
||||
|
||||
if (!imageSizes || !imageSizes[requestedSize]) {
|
||||
return imagePath;
|
||||
}
|
||||
|
||||
const {width, height} = imageSizes[requestedSize];
|
||||
|
||||
if (!width && !height) {
|
||||
return imagePath;
|
||||
}
|
||||
|
||||
const [imgBlogUrl, imageName] = imagePath.split(STATIC_IMAGE_URL_PREFIX);
|
||||
|
||||
const sizeDirectoryName = prefixIfPresent('w', width) + prefixIfPresent('h', height);
|
||||
const formatPrefix = requestedFormat && imageTransform.canTransformToFormat(requestedFormat) ? `/format/${requestedFormat}` : '';
|
||||
|
||||
return [imgBlogUrl, STATIC_IMAGE_URL_PREFIX, `/size/${sizeDirectoryName}`, formatPrefix, imageName].join('');
|
||||
}
|
||||
|
||||
function prefixIfPresent(prefix, string) {
|
||||
return string ? prefix + string : '';
|
||||
}
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
const _ = require('lodash');
|
||||
const {getImageWithSize} = require('../utils/images');
|
||||
const config = require('../../shared/config');
|
||||
const imageSizeCache = require('../../server/lib/image').cachedImageSizeFromUrl;
|
||||
|
||||
/**
|
||||
|
@ -9,23 +11,28 @@ const imageSizeCache = require('../../server/lib/image').cachedImageSizeFromUrl;
|
|||
* called to receive image width and height
|
||||
*/
|
||||
async function getImageDimensions(metaData) {
|
||||
const MAX_SOCIAL_IMG_WIDTH = config.get('imageOptimization:internalImageSizes:social-image:width') || 1200;
|
||||
|
||||
const fetch = {
|
||||
coverImage: imageSizeCache.getCachedImageSizeFromUrl(metaData.coverImage.url),
|
||||
authorImage: imageSizeCache.getCachedImageSizeFromUrl(metaData.authorImage.url),
|
||||
ogImage: imageSizeCache.getCachedImageSizeFromUrl(metaData.ogImage.url),
|
||||
twitterImage: imageSizeCache.getCachedImageSizeFromUrl(metaData.twitterImage),
|
||||
logo: imageSizeCache.getCachedImageSizeFromUrl(metaData.site.logo.url)
|
||||
};
|
||||
|
||||
const [coverImage, authorImage, ogImage, logo] = await Promise.all([
|
||||
const [coverImage, authorImage, ogImage, twitterImage, logo] = await Promise.all([
|
||||
fetch.coverImage,
|
||||
fetch.authorImage,
|
||||
fetch.ogImage,
|
||||
fetch.twitterImage,
|
||||
fetch.logo
|
||||
]);
|
||||
const imageObj = {
|
||||
coverImage,
|
||||
authorImage,
|
||||
ogImage,
|
||||
twitterImage,
|
||||
logo
|
||||
};
|
||||
|
||||
|
@ -53,12 +60,33 @@ async function getImageDimensions(metaData) {
|
|||
});
|
||||
}
|
||||
} else {
|
||||
_.assign(metaData[value], {
|
||||
dimensions: {
|
||||
width: key.width,
|
||||
height: key.height
|
||||
if (key.width > MAX_SOCIAL_IMG_WIDTH) {
|
||||
const ratio = key.height / key.width;
|
||||
key.width = MAX_SOCIAL_IMG_WIDTH;
|
||||
key.height = Math.round(MAX_SOCIAL_IMG_WIDTH * ratio);
|
||||
|
||||
const sizeOptions = {
|
||||
requestedSize: `social-image`,
|
||||
imageSizes: config.get('imageOptimization:internalImageSizes')
|
||||
};
|
||||
|
||||
if (typeof metaData[value] === 'string') {
|
||||
const url = getImageWithSize(metaData[value], sizeOptions);
|
||||
metaData[value] = url;
|
||||
} else {
|
||||
const url = getImageWithSize(metaData[value].url, sizeOptions);
|
||||
_.assign(metaData[value], {url});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof metaData[value] === 'object') {
|
||||
_.assign(metaData[value], {
|
||||
dimensions: {
|
||||
width: key.width,
|
||||
height: key.height
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
100
ghost/core/core/frontend/utils/images.js
Normal file
100
ghost/core/core/frontend/utils/images.js
Normal file
|
@ -0,0 +1,100 @@
|
|||
const url = require('url');
|
||||
const imageTransform = require('@tryghost/image-transform');
|
||||
const urlUtils = require('../../shared/url-utils');
|
||||
|
||||
module.exports.detectInternalImage = function detectInternalImage(requestedImageUrl) {
|
||||
const siteUrl = urlUtils.getSiteUrl();
|
||||
const isAbsoluteImage = /https?:\/\//.test(requestedImageUrl);
|
||||
const isAbsoluteInternalImage = isAbsoluteImage && requestedImageUrl.startsWith(siteUrl);
|
||||
|
||||
// CASE: imagePath is a "protocol relative" url e.g. "//www.gravatar.com/ava..."
|
||||
// by resolving the the imagePath relative to the blog url, we can then
|
||||
// detect if the imagePath is external, or internal.
|
||||
const isRelativeInternalImage = !isAbsoluteImage && url.resolve(siteUrl, requestedImageUrl).startsWith(siteUrl);
|
||||
|
||||
return isAbsoluteInternalImage || isRelativeInternalImage;
|
||||
};
|
||||
|
||||
module.exports.detectUnsplashImage = function detectUnsplashImage(requestedImageUrl) {
|
||||
const isUnsplashImage = /images\.unsplash\.com/.test(requestedImageUrl);
|
||||
return isUnsplashImage;
|
||||
};
|
||||
|
||||
module.exports.getUnsplashImage = function getUnsplashImage(imagePath, sizeOptions) {
|
||||
const parsedUrl = new URL(imagePath);
|
||||
const {requestedSize, imageSizes, requestedFormat} = sizeOptions;
|
||||
|
||||
if (requestedFormat) {
|
||||
const supportedFormats = ['avif', 'gif', 'jpg', 'png', 'webp'];
|
||||
if (supportedFormats.includes(requestedFormat)) {
|
||||
parsedUrl.searchParams.set('fm', requestedFormat);
|
||||
} else if (requestedFormat === 'jpeg') {
|
||||
// Map to alias
|
||||
parsedUrl.searchParams.set('fm', 'jpg');
|
||||
}
|
||||
}
|
||||
|
||||
if (!imageSizes || !imageSizes[requestedSize]) {
|
||||
return parsedUrl.toString();
|
||||
}
|
||||
|
||||
const {width, height} = imageSizes[requestedSize];
|
||||
|
||||
if (!width && !height) {
|
||||
return parsedUrl.toString();
|
||||
}
|
||||
|
||||
parsedUrl.searchParams.delete('w');
|
||||
parsedUrl.searchParams.delete('h');
|
||||
|
||||
if (width) {
|
||||
parsedUrl.searchParams.set('w', width);
|
||||
}
|
||||
if (height) {
|
||||
parsedUrl.searchParams.set('h', height);
|
||||
}
|
||||
return parsedUrl.toString();
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} imagePath
|
||||
* @param {Object} sizeOptions
|
||||
* @param {string} sizeOptions.requestedSize
|
||||
* @param {Object[]} sizeOptions.imageSizes
|
||||
* @param {string} [sizeOptions.requestedFormat]
|
||||
* @returns
|
||||
*/
|
||||
module.exports.getImageWithSize = function getImageWithSize(imagePath, sizeOptions) {
|
||||
const hasLeadingSlash = imagePath[0] === '/';
|
||||
|
||||
if (hasLeadingSlash) {
|
||||
return '/' + getImageWithSize(imagePath.slice(1), sizeOptions);
|
||||
}
|
||||
const {requestedSize, imageSizes, requestedFormat} = sizeOptions;
|
||||
|
||||
if (!requestedSize) {
|
||||
return imagePath;
|
||||
}
|
||||
|
||||
if (!imageSizes || !imageSizes[requestedSize]) {
|
||||
return imagePath;
|
||||
}
|
||||
|
||||
const {width, height} = imageSizes[requestedSize];
|
||||
|
||||
if (!width && !height) {
|
||||
return imagePath;
|
||||
}
|
||||
|
||||
const [imgBlogUrl, imageName] = imagePath.split(urlUtils.STATIC_IMAGE_URL_PREFIX);
|
||||
|
||||
const sizeDirectoryName = prefixIfPresent('w', width) + prefixIfPresent('h', height);
|
||||
const formatPrefix = requestedFormat && imageTransform.canTransformToFormat(requestedFormat) ? `/format/${requestedFormat}` : '';
|
||||
|
||||
return [imgBlogUrl, urlUtils.STATIC_IMAGE_URL_PREFIX, `/size/${sizeDirectoryName}`, formatPrefix, imageName].join('');
|
||||
};
|
||||
|
||||
function prefixIfPresent(prefix, string) {
|
||||
return string ? prefix + string : '';
|
||||
}
|
|
@ -124,7 +124,8 @@
|
|||
"signup-form-icon": {"width": 192, "height": 192},
|
||||
"email-header-image": {"width": 1200},
|
||||
"email-latest-posts-image": {"width": 200, "height": 200},
|
||||
"email-latest-posts-image-mobile": {"width": 1200, "height": 960}
|
||||
"email-latest-posts-image-mobile": {"width": 1200, "height": 960},
|
||||
"social-image": {"width": 1200}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
const _ = require('lodash');
|
||||
const should = require('should');
|
||||
const sinon = require('sinon');
|
||||
const rewire = require('rewire');
|
||||
|
@ -218,4 +219,67 @@ describe('getImageDimensions', function () {
|
|||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
it('should adjust image sizes to a max width', function (done) {
|
||||
const originalMetaData = {
|
||||
coverImage: {
|
||||
url: 'http://mysite.com/content/images/mypostcoverimage.jpg'
|
||||
},
|
||||
authorImage: {
|
||||
url: 'http://mysite.com/content/images/me.jpg'
|
||||
},
|
||||
ogImage: {
|
||||
url: 'http://mysite.com/content/images/super-facebook-image.jpg'
|
||||
},
|
||||
twitterImage: 'http://mysite.com/content/images/super-twitter-image.jpg',
|
||||
site: {
|
||||
logo: {
|
||||
url: 'http://mysite.com/content/images/logo.jpg'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// getImageDimensions modifies metaData so we clone so we can compare
|
||||
// against the original
|
||||
const metaData = _.cloneDeep(originalMetaData);
|
||||
|
||||
// callsFake rather than returns otherwise the object is passed by
|
||||
// reference and assigned to each image meaning it gets modified when
|
||||
// the first image is resized and later images no longer look oversized
|
||||
sizeOfStub.callsFake(() => ({
|
||||
width: 2000,
|
||||
height: 1200,
|
||||
type: 'jpg'
|
||||
}));
|
||||
|
||||
getImageDimensions.__set__('imageSizeCache', {
|
||||
getCachedImageSizeFromUrl: sizeOfStub
|
||||
});
|
||||
|
||||
getImageDimensions(metaData).then(function (result) {
|
||||
should.exist(result);
|
||||
sizeOfStub.calledWith(originalMetaData.coverImage.url).should.be.true();
|
||||
sizeOfStub.calledWith(originalMetaData.authorImage.url).should.be.true();
|
||||
sizeOfStub.calledWith(originalMetaData.ogImage.url).should.be.true();
|
||||
sizeOfStub.calledWith(originalMetaData.twitterImage).should.be.true();
|
||||
sizeOfStub.calledWith(originalMetaData.site.logo.url).should.be.true();
|
||||
result.coverImage.should.have.property('url');
|
||||
result.coverImage.url.should.eql('http://mysite.com/content/images/size/w1200/mypostcoverimage.jpg');
|
||||
result.coverImage.should.have.property('dimensions');
|
||||
result.coverImage.dimensions.should.have.property('width', 1200);
|
||||
result.coverImage.dimensions.should.have.property('height', 720);
|
||||
result.authorImage.should.have.property('url');
|
||||
result.authorImage.url.should.eql('http://mysite.com/content/images/size/w1200/me.jpg');
|
||||
result.authorImage.should.have.property('dimensions');
|
||||
result.authorImage.dimensions.should.have.property('width', 1200);
|
||||
result.authorImage.dimensions.should.have.property('height', 720);
|
||||
result.ogImage.should.have.property('url');
|
||||
result.ogImage.url.should.eql('http://mysite.com/content/images/size/w1200/super-facebook-image.jpg');
|
||||
result.ogImage.should.have.property('dimensions');
|
||||
result.ogImage.dimensions.should.have.property('width', 1200);
|
||||
result.ogImage.dimensions.should.have.property('height', 720);
|
||||
result.twitterImage.should.eql('http://mysite.com/content/images/size/w1200/super-twitter-image.jpg');
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Add table
Reference in a new issue