0
Fork 0
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:
Kevin Ansfield 2023-11-20 12:39:51 +00:00 committed by GitHub
parent 0cf25d0afe
commit 1be490ae9e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 207 additions and 104 deletions

View file

@ -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 : '';
}

View file

@ -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
}
});
}
}
}
});

View 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 : '';
}

View file

@ -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}
}
}
}

View file

@ -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);
});
});